Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/agents/refactor-cleaner.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions .claude/skills/backend-patterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}]
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions src/http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .handlers import register_exception_handlers
from .response import Response

__all__ = ["Response", "register_exception_handlers"]
2 changes: 1 addition & 1 deletion src/handlers.py → src/http/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/responses/middleware.py → src/http/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 4 additions & 4 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 0 additions & 4 deletions src/responses/__init__.py

This file was deleted.

8 changes: 4 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,24 @@ 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


@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


@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)
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/test_audit_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/test_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/test_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
File renamed without changes.
10 changes: 5 additions & 5 deletions tests/responses/test_base.py → tests/test_http/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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()
Expand All @@ -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"
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading