agentic-loop 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/.claude/commands/explain.md +114 -0
  2. package/.claude/commands/idea.md +398 -0
  3. package/.claude/commands/my-dna.md +122 -0
  4. package/.claude/commands/prd.md +286 -0
  5. package/.claude/commands/review.md +167 -0
  6. package/.claude/commands/sign.md +32 -0
  7. package/.claude/commands/styleguide.md +450 -0
  8. package/.claude/commands/tour.md +301 -0
  9. package/.claude/commands/vibe-check.md +116 -0
  10. package/.claude/commands/vibe-help.md +47 -0
  11. package/.claude/commands/vibe-list.md +203 -0
  12. package/.pre-commit-hooks.yaml +102 -0
  13. package/LICENSE +21 -0
  14. package/README.md +238 -0
  15. package/bin/agentic-loop.sh +24 -0
  16. package/bin/postinstall.sh +29 -0
  17. package/bin/ralph.sh +171 -0
  18. package/bin/vibe-check.js +19 -0
  19. package/dist/checks/check-any-types.d.ts +6 -0
  20. package/dist/checks/check-any-types.d.ts.map +1 -0
  21. package/dist/checks/check-any-types.js +73 -0
  22. package/dist/checks/check-any-types.js.map +1 -0
  23. package/dist/checks/check-commented-code.d.ts +6 -0
  24. package/dist/checks/check-commented-code.d.ts.map +1 -0
  25. package/dist/checks/check-commented-code.js +81 -0
  26. package/dist/checks/check-commented-code.js.map +1 -0
  27. package/dist/checks/check-console-error.d.ts +6 -0
  28. package/dist/checks/check-console-error.d.ts.map +1 -0
  29. package/dist/checks/check-console-error.js +41 -0
  30. package/dist/checks/check-console-error.js.map +1 -0
  31. package/dist/checks/check-debug-statements.d.ts +6 -0
  32. package/dist/checks/check-debug-statements.d.ts.map +1 -0
  33. package/dist/checks/check-debug-statements.js +120 -0
  34. package/dist/checks/check-debug-statements.js.map +1 -0
  35. package/dist/checks/check-deep-nesting.d.ts +6 -0
  36. package/dist/checks/check-deep-nesting.d.ts.map +1 -0
  37. package/dist/checks/check-deep-nesting.js +116 -0
  38. package/dist/checks/check-deep-nesting.js.map +1 -0
  39. package/dist/checks/check-docker-platform.d.ts +6 -0
  40. package/dist/checks/check-docker-platform.d.ts.map +1 -0
  41. package/dist/checks/check-docker-platform.js +42 -0
  42. package/dist/checks/check-docker-platform.js.map +1 -0
  43. package/dist/checks/check-dry-violations.d.ts +6 -0
  44. package/dist/checks/check-dry-violations.d.ts.map +1 -0
  45. package/dist/checks/check-dry-violations.js +124 -0
  46. package/dist/checks/check-dry-violations.js.map +1 -0
  47. package/dist/checks/check-empty-catch.d.ts +6 -0
  48. package/dist/checks/check-empty-catch.d.ts.map +1 -0
  49. package/dist/checks/check-empty-catch.js +111 -0
  50. package/dist/checks/check-empty-catch.js.map +1 -0
  51. package/dist/checks/check-function-length.d.ts +6 -0
  52. package/dist/checks/check-function-length.d.ts.map +1 -0
  53. package/dist/checks/check-function-length.js +152 -0
  54. package/dist/checks/check-function-length.js.map +1 -0
  55. package/dist/checks/check-hardcoded-ai-models.d.ts +10 -0
  56. package/dist/checks/check-hardcoded-ai-models.d.ts.map +1 -0
  57. package/dist/checks/check-hardcoded-ai-models.js +102 -0
  58. package/dist/checks/check-hardcoded-ai-models.js.map +1 -0
  59. package/dist/checks/check-hardcoded-urls.d.ts +6 -0
  60. package/dist/checks/check-hardcoded-urls.d.ts.map +1 -0
  61. package/dist/checks/check-hardcoded-urls.js +124 -0
  62. package/dist/checks/check-hardcoded-urls.js.map +1 -0
  63. package/dist/checks/check-magic-numbers.d.ts +6 -0
  64. package/dist/checks/check-magic-numbers.d.ts.map +1 -0
  65. package/dist/checks/check-magic-numbers.js +116 -0
  66. package/dist/checks/check-magic-numbers.js.map +1 -0
  67. package/dist/checks/check-secrets.d.ts +6 -0
  68. package/dist/checks/check-secrets.d.ts.map +1 -0
  69. package/dist/checks/check-secrets.js +138 -0
  70. package/dist/checks/check-secrets.js.map +1 -0
  71. package/dist/checks/check-snake-case-ts.d.ts +6 -0
  72. package/dist/checks/check-snake-case-ts.d.ts.map +1 -0
  73. package/dist/checks/check-snake-case-ts.js +78 -0
  74. package/dist/checks/check-snake-case-ts.js.map +1 -0
  75. package/dist/checks/check-todo-fixme.d.ts +6 -0
  76. package/dist/checks/check-todo-fixme.d.ts.map +1 -0
  77. package/dist/checks/check-todo-fixme.js +41 -0
  78. package/dist/checks/check-todo-fixme.js.map +1 -0
  79. package/dist/checks/check-unsafe-html.d.ts +6 -0
  80. package/dist/checks/check-unsafe-html.d.ts.map +1 -0
  81. package/dist/checks/check-unsafe-html.js +101 -0
  82. package/dist/checks/check-unsafe-html.js.map +1 -0
  83. package/dist/checks/index.d.ts +30 -0
  84. package/dist/checks/index.d.ts.map +1 -0
  85. package/dist/checks/index.js +57 -0
  86. package/dist/checks/index.js.map +1 -0
  87. package/dist/cli.d.ts +13 -0
  88. package/dist/cli.d.ts.map +1 -0
  89. package/dist/cli.js +208 -0
  90. package/dist/cli.js.map +1 -0
  91. package/dist/index.d.ts +9 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +10 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/utils/file-reader.d.ts +24 -0
  96. package/dist/utils/file-reader.d.ts.map +1 -0
  97. package/dist/utils/file-reader.js +146 -0
  98. package/dist/utils/file-reader.js.map +1 -0
  99. package/dist/utils/patterns.d.ts +27 -0
  100. package/dist/utils/patterns.d.ts.map +1 -0
  101. package/dist/utils/patterns.js +84 -0
  102. package/dist/utils/patterns.js.map +1 -0
  103. package/dist/utils/reporters.d.ts +21 -0
  104. package/dist/utils/reporters.d.ts.map +1 -0
  105. package/dist/utils/reporters.js +115 -0
  106. package/dist/utils/reporters.js.map +1 -0
  107. package/dist/utils/types.d.ts +71 -0
  108. package/dist/utils/types.d.ts.map +1 -0
  109. package/dist/utils/types.js +5 -0
  110. package/dist/utils/types.js.map +1 -0
  111. package/package.json +83 -0
  112. package/ralph/api.sh +216 -0
  113. package/ralph/backup.sh +838 -0
  114. package/ralph/browser-verify/README.md +135 -0
  115. package/ralph/browser-verify/verify.ts +450 -0
  116. package/ralph/checks/check-fastapi-responses.py +155 -0
  117. package/ralph/hooks/hooks-config.json +72 -0
  118. package/ralph/hooks/inject-context.sh +44 -0
  119. package/ralph/hooks/install.sh +207 -0
  120. package/ralph/hooks/log-tools.sh +45 -0
  121. package/ralph/hooks/protect-prd.sh +27 -0
  122. package/ralph/hooks/save-learnings.sh +36 -0
  123. package/ralph/hooks/warn-debug.sh +54 -0
  124. package/ralph/hooks/warn-empty-catch.sh +63 -0
  125. package/ralph/hooks/warn-secrets.sh +89 -0
  126. package/ralph/hooks/warn-urls.sh +77 -0
  127. package/ralph/init.sh +515 -0
  128. package/ralph/loop.sh +730 -0
  129. package/ralph/playwright.sh +238 -0
  130. package/ralph/prd.sh +295 -0
  131. package/ralph/setup/feature-tour.sh +155 -0
  132. package/ralph/setup/quick-setup.sh +239 -0
  133. package/ralph/setup/tutorial.sh +159 -0
  134. package/ralph/setup/ui.sh +136 -0
  135. package/ralph/setup.sh +401 -0
  136. package/ralph/signs.sh +150 -0
  137. package/ralph/utils.sh +682 -0
  138. package/ralph/verify/browser.sh +324 -0
  139. package/ralph/verify/lint.sh +363 -0
  140. package/ralph/verify/review.sh +152 -0
  141. package/ralph/verify/tests.sh +81 -0
  142. package/ralph/verify.sh +268 -0
  143. package/templates/PROMPT.md +235 -0
  144. package/templates/config/fullstack.json +86 -0
  145. package/templates/config/go.json +81 -0
  146. package/templates/config/minimal.json +76 -0
  147. package/templates/config/node.json +81 -0
  148. package/templates/config/python.json +81 -0
  149. package/templates/config/rust.json +81 -0
  150. package/templates/examples/CLAUDE-django.md +174 -0
  151. package/templates/examples/CLAUDE-fastapi.md +270 -0
  152. package/templates/examples/CLAUDE-fastmcp.md +352 -0
  153. package/templates/examples/CLAUDE-fullstack.md +256 -0
  154. package/templates/examples/CLAUDE-node.md +246 -0
  155. package/templates/examples/CLAUDE-react.md +138 -0
  156. package/templates/optional/cursorrules.template +147 -0
  157. package/templates/optional/eslint.config.js +34 -0
  158. package/templates/optional/lint-staged.config.js +34 -0
  159. package/templates/optional/ruff.toml +125 -0
  160. package/templates/optional/vibe-check.yml +116 -0
  161. package/templates/optional/vscode-settings.json +127 -0
  162. package/templates/signs.json +46 -0
@@ -0,0 +1,81 @@
1
+ {
2
+ "auth": {
3
+ "testUser": "",
4
+ "testPassword": "",
5
+ "loginEndpoint": "/api/auth/login",
6
+ "loginMethod": "POST",
7
+ "tokenType": "jwt",
8
+ "tokenHeader": "Authorization",
9
+ "tokenPrefix": "Bearer"
10
+ },
11
+
12
+ "docker": {
13
+ "enabled": false,
14
+ "composeFile": "docker-compose.yml",
15
+ "serviceName": "app",
16
+ "execPrefix": "docker compose exec -T"
17
+ },
18
+
19
+ "paths": {
20
+ "src": "src",
21
+ "tests": "tests",
22
+ "e2e": "tests/e2e"
23
+ },
24
+
25
+ "commands": {
26
+ "dev": "cargo run",
27
+ "install": "cargo build",
28
+ "seed": "",
29
+ "resetDb": ""
30
+ },
31
+
32
+ "migrations": {
33
+ "command": "sqlx migrate run",
34
+ "pattern": "migrations/.*\\.sql$"
35
+ },
36
+
37
+ "checks": {
38
+ "build": "cargo build",
39
+ "lint": "cargo clippy -- -D warnings",
40
+ "test": "cargo test"
41
+ },
42
+
43
+ "api": {
44
+ "baseUrl": "http://localhost:8080",
45
+ "healthEndpoint": "/health",
46
+ "timeout": 30
47
+ },
48
+
49
+ "playwright": {
50
+ "enabled": false,
51
+ "testDir": "tests/e2e",
52
+ "projects": ["chromium"],
53
+ "baseUrl": "http://localhost:8080"
54
+ },
55
+
56
+ "verification": {
57
+ "codeReviewEnabled": true,
58
+ "browserEnabled": true,
59
+ "a11yEnabled": false,
60
+ "mobileViewport": 375,
61
+ "screenshotOnFailure": true
62
+ },
63
+
64
+ "urls": {
65
+ "app": "http://localhost:8080",
66
+ "docs": "http://localhost:8080/swagger"
67
+ },
68
+
69
+ "env": {
70
+ "required": ["DATABASE_URL"],
71
+ "optional": ["REDIS_URL"]
72
+ },
73
+
74
+ "maxIterations": 20,
75
+ "maxSessionSeconds": 600,
76
+
77
+ "contextRotThreshold": {
78
+ "maxStories": 10,
79
+ "maxFilesChanged": 20
80
+ }
81
+ }
@@ -0,0 +1,174 @@
1
+ # Project Instructions for AI Coding Agents
2
+
3
+ ## Naming Conventions
4
+ - **Files**: `snake_case.py` — e.g., `user_views.py`, `auth_serializers.py`
5
+ - **Functions/Variables**: `snake_case` — e.g., `get_user_by_id`, `is_authenticated`
6
+ - **Classes**: `PascalCase` — e.g., `UserViewSet`, `AuthSerializer`
7
+ - **Models**: `PascalCase` singular — e.g., `User`, `BlogPost` (Django pluralizes table names)
8
+ - **Constants**: `SCREAMING_SNAKE` — e.g., `MAX_UPLOAD_SIZE`, `DEFAULT_PAGE_SIZE`
9
+ - **URL patterns**: `kebab-case` — e.g., `/api/user-profile/`, `/api/blog-posts/`
10
+ - **Template files**: `snake_case.html` — e.g., `user_detail.html`
11
+
12
+ ## Tech Stack
13
+ - **Backend**: Django 5, Django REST Framework
14
+ - **Database**: PostgreSQL
15
+ - **Cache/Queue**: Redis, Celery
16
+ - **Testing**: pytest, pytest-django
17
+
18
+ ## Code Quality Standards
19
+
20
+ ### Python Style
21
+ - Follow PEP 8
22
+ - Use type hints for function signatures
23
+ - Maximum line length: 88 (Black default)
24
+ - Use f-strings for string formatting
25
+
26
+ ```python
27
+ # Good
28
+ def get_user_by_email(email: str) -> User | None:
29
+ """Fetch a user by their email address."""
30
+ return User.objects.filter(email=email).first()
31
+ ```
32
+
33
+ ### Django Models
34
+ - Use explicit `related_name` on ForeignKey/M2M
35
+ - Add `__str__` method to all models
36
+ - Use model managers for complex queries
37
+ - Add indexes for frequently queried fields
38
+
39
+ ```python
40
+ class Order(models.Model):
41
+ user = models.ForeignKey(
42
+ User,
43
+ on_delete=models.CASCADE,
44
+ related_name='orders'
45
+ )
46
+ created_at = models.DateTimeField(auto_now_add=True, db_index=True)
47
+
48
+ class Meta:
49
+ ordering = ['-created_at']
50
+
51
+ def __str__(self):
52
+ return f"Order {self.id} by {self.user.email}"
53
+ ```
54
+
55
+ ### Django REST Framework
56
+ - Use serializers for all input/output
57
+ - Use ViewSets for CRUD operations
58
+ - Use proper permission classes
59
+ - Return appropriate HTTP status codes
60
+
61
+ ```python
62
+ class OrderViewSet(viewsets.ModelViewSet):
63
+ serializer_class = OrderSerializer
64
+ permission_classes = [IsAuthenticated]
65
+
66
+ def get_queryset(self):
67
+ # Only return user's own orders
68
+ return Order.objects.filter(user=self.request.user)
69
+
70
+ def perform_create(self, serializer):
71
+ serializer.save(user=self.request.user)
72
+ ```
73
+
74
+ ### Error Handling
75
+ - Use DRF exceptions for API errors
76
+ - Log errors with context
77
+ - Never expose internal errors to users
78
+ - Handle specific exceptions, not bare except
79
+
80
+ ```python
81
+ # Bad
82
+ try:
83
+ process_order(order)
84
+ except Exception:
85
+ pass
86
+
87
+ # Good
88
+ try:
89
+ process_order(order)
90
+ except PaymentError as e:
91
+ logger.error(f"Payment failed for order {order.id}: {e}")
92
+ raise ValidationError({"payment": "Payment processing failed"})
93
+ except InventoryError as e:
94
+ logger.warning(f"Inventory issue for order {order.id}: {e}")
95
+ raise ValidationError({"inventory": str(e)})
96
+ ```
97
+
98
+ ### Database Queries
99
+ - Avoid N+1 queries (use select_related/prefetch_related)
100
+ - Use .only() or .defer() for large models
101
+ - Use database transactions for multi-step operations
102
+ - Add indexes for filter/order fields
103
+
104
+ ```python
105
+ # Bad - N+1 query
106
+ orders = Order.objects.all()
107
+ for order in orders:
108
+ print(order.user.email) # Hits DB for each order
109
+
110
+ # Good
111
+ orders = Order.objects.select_related('user').all()
112
+ for order in orders:
113
+ print(order.user.email) # No additional queries
114
+ ```
115
+
116
+ ### Security
117
+ - Use Django's CSRF protection
118
+ - Validate all user input with serializers
119
+ - Use parameterized queries (Django ORM does this)
120
+ - Never trust user input for file paths or shell commands
121
+
122
+ ### Testing
123
+ - Use pytest and pytest-django
124
+ - Use fixtures for test data
125
+ - Test API endpoints with APIClient
126
+ - Mock external services
127
+
128
+ ```python
129
+ @pytest.mark.django_db
130
+ class TestOrderAPI:
131
+ def test_create_order(self, authenticated_client, product):
132
+ response = authenticated_client.post('/api/orders/', {
133
+ 'product_id': product.id,
134
+ 'quantity': 2,
135
+ })
136
+ assert response.status_code == 201
137
+ assert Order.objects.count() == 1
138
+
139
+ def test_cannot_create_order_unauthenticated(self, client):
140
+ response = client.post('/api/orders/', {})
141
+ assert response.status_code == 401
142
+ ```
143
+
144
+ ## File Structure
145
+ ```
146
+ project/
147
+ ├── config/ # Django settings, URLs, WSGI
148
+ ├── apps/
149
+ │ ├── users/ # User-related models, views
150
+ │ ├── orders/ # Order-related models, views
151
+ │ └── common/ # Shared utilities
152
+ ├── tests/ # Test files
153
+ └── manage.py
154
+ ```
155
+
156
+ ## Environment Variables
157
+ ```python
158
+ # settings.py
159
+ DATABASE_URL = os.getenv('DATABASE_URL')
160
+ SECRET_KEY = os.getenv('SECRET_KEY')
161
+ DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
162
+ ```
163
+
164
+ ## Pre-commit Hooks
165
+ This project uses agentic-loop hooks. Run `/vibe-check` before committing.
166
+
167
+ ## Common Commands
168
+ ```bash
169
+ make up # Start Docker services
170
+ make migrate # Run migrations
171
+ make test # Run tests
172
+ make shell # Django shell
173
+ make logs # View logs
174
+ ```
@@ -0,0 +1,270 @@
1
+ # CLAUDE.md - FastAPI Project
2
+
3
+ ## Naming Conventions
4
+ - **Files**: `snake_case.py` — e.g., `user_service.py`, `auth_router.py`
5
+ - **Functions/Variables**: `snake_case` — e.g., `get_user_by_id`, `is_valid`
6
+ - **Classes**: `PascalCase` — e.g., `UserService`, `AuthRouter`
7
+ - **Pydantic Models**: `PascalCase` — e.g., `UserCreate`, `UserResponse`
8
+ - **Constants**: `SCREAMING_SNAKE` — e.g., `MAX_RETRIES`, `DEFAULT_LIMIT`
9
+ - **Database tables**: `snake_case` — e.g., `user_sessions`, `api_keys`
10
+ - **API endpoints**: `kebab-case` — e.g., `/api/user-profile`, `/api/v1/auth`
11
+
12
+ ## Project Overview
13
+
14
+ This is a FastAPI backend application with async SQLAlchemy and Pydantic.
15
+
16
+ ## Tech Stack
17
+
18
+ - **Framework**: FastAPI
19
+ - **ORM**: SQLAlchemy 2.0 (async)
20
+ - **Validation**: Pydantic v2
21
+ - **Database**: PostgreSQL
22
+ - **Migrations**: Alembic
23
+ - **Testing**: pytest + httpx
24
+ - **Task Queue**: Celery + Redis (if applicable)
25
+
26
+ ## Project Structure
27
+
28
+ ```
29
+ app/
30
+ ├── main.py # FastAPI app entry point
31
+ ├── config.py # Settings via pydantic-settings
32
+ ├── database.py # Async SQLAlchemy setup
33
+ ├── models/ # SQLAlchemy models
34
+ ├── schemas/ # Pydantic schemas
35
+ ├── routers/ # API route handlers
36
+ ├── services/ # Business logic
37
+ ├── dependencies.py # FastAPI dependencies
38
+ └── exceptions.py # Custom exceptions
39
+ tests/
40
+ ├── conftest.py # Fixtures
41
+ ├── test_api/ # API tests
42
+ └── test_services/ # Unit tests
43
+ ```
44
+
45
+ ## Commands
46
+
47
+ ```bash
48
+ # Development
49
+ uvicorn app.main:app --reload --port 8000
50
+
51
+ # Database
52
+ alembic upgrade head # Run migrations
53
+ alembic revision --autogenerate -m "" # Create migration
54
+
55
+ # Testing
56
+ pytest # Run all tests
57
+ pytest -x # Stop on first failure
58
+ pytest --cov=app # With coverage
59
+
60
+ # Linting
61
+ ruff check app/
62
+ ruff format app/
63
+ ```
64
+
65
+ ## Code Standards
66
+
67
+ ### API Endpoints
68
+
69
+ ```python
70
+ # Good - explicit status codes, response model, dependencies
71
+ @router.post("/users", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
72
+ async def create_user(
73
+ user_in: UserCreate,
74
+ db: AsyncSession = Depends(get_db),
75
+ current_user: User = Depends(get_current_user),
76
+ ) -> UserResponse:
77
+ """Create a new user."""
78
+ return await user_service.create(db, user_in)
79
+
80
+ # Bad - implicit everything
81
+ @router.post("/users")
82
+ async def create_user(user_in: UserCreate, db = Depends(get_db)):
83
+ return await create(db, user_in)
84
+ ```
85
+
86
+ ### Pydantic Schemas
87
+
88
+ ```python
89
+ # Good - explicit validation, examples, field descriptions
90
+ class UserCreate(BaseModel):
91
+ email: EmailStr = Field(..., description="User's email address")
92
+ password: str = Field(..., min_length=8, description="Password (min 8 chars)")
93
+ name: str = Field(..., min_length=1, max_length=100)
94
+
95
+ model_config = ConfigDict(
96
+ json_schema_extra={
97
+ "example": {
98
+ "email": "user@example.com",
99
+ "password": "securepass123",
100
+ "name": "John Doe"
101
+ }
102
+ }
103
+ )
104
+
105
+ # Bad - no validation
106
+ class UserCreate(BaseModel):
107
+ email: str
108
+ password: str
109
+ name: str
110
+ ```
111
+
112
+ ### SQLAlchemy Models
113
+
114
+ ```python
115
+ # Good - explicit types, relationships, indexes
116
+ class User(Base):
117
+ __tablename__ = "users"
118
+
119
+ id: Mapped[int] = mapped_column(primary_key=True)
120
+ email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
121
+ hashed_password: Mapped[str] = mapped_column(String(255))
122
+ is_active: Mapped[bool] = mapped_column(default=True)
123
+ created_at: Mapped[datetime] = mapped_column(default=func.now())
124
+
125
+ # Relationships
126
+ posts: Mapped[list["Post"]] = relationship(back_populates="author")
127
+
128
+ # Bad - old style, no types
129
+ class User(Base):
130
+ __tablename__ = "users"
131
+ id = Column(Integer, primary_key=True)
132
+ email = Column(String)
133
+ password = Column(String)
134
+ ```
135
+
136
+ ### Async Database Sessions
137
+
138
+ ```python
139
+ # Good - async context manager
140
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
141
+ async with async_session() as session:
142
+ try:
143
+ yield session
144
+ await session.commit()
145
+ except Exception:
146
+ await session.rollback()
147
+ raise
148
+
149
+ # Bad - no cleanup
150
+ async def get_db():
151
+ return async_session()
152
+ ```
153
+
154
+ ### Error Handling
155
+
156
+ ```python
157
+ # Good - custom exceptions with proper HTTP codes
158
+ class NotFoundError(Exception):
159
+ def __init__(self, resource: str, id: int):
160
+ self.resource = resource
161
+ self.id = id
162
+
163
+ @app.exception_handler(NotFoundError)
164
+ async def not_found_handler(request: Request, exc: NotFoundError):
165
+ return JSONResponse(
166
+ status_code=404,
167
+ content={"detail": f"{exc.resource} with id {exc.id} not found"}
168
+ )
169
+
170
+ # Bad - generic exceptions
171
+ raise Exception("User not found")
172
+ ```
173
+
174
+ ### Service Layer
175
+
176
+ ```python
177
+ # Good - service handles business logic
178
+ class UserService:
179
+ async def create(self, db: AsyncSession, user_in: UserCreate) -> User:
180
+ # Check if exists
181
+ existing = await self.get_by_email(db, user_in.email)
182
+ if existing:
183
+ raise ConflictError("User", "email", user_in.email)
184
+
185
+ # Hash password
186
+ hashed = hash_password(user_in.password)
187
+
188
+ # Create user
189
+ user = User(email=user_in.email, hashed_password=hashed, name=user_in.name)
190
+ db.add(user)
191
+ await db.flush()
192
+ return user
193
+
194
+ # Bad - logic in router
195
+ @router.post("/users")
196
+ async def create_user(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
197
+ existing = await db.execute(select(User).where(User.email == user_in.email))
198
+ if existing.scalar():
199
+ raise HTTPException(409, "Email exists")
200
+ hashed = bcrypt.hash(user_in.password)
201
+ user = User(email=user_in.email, hashed_password=hashed)
202
+ db.add(user)
203
+ # ... more logic
204
+ ```
205
+
206
+ ## Do NOT
207
+
208
+ - Use `Any` type - create proper Pydantic models
209
+ - Put business logic in routers - use service layer
210
+ - Use synchronous database calls in async endpoints
211
+ - Hardcode secrets - use `pydantic-settings` with env vars
212
+ - Skip input validation - use Pydantic Field validators
213
+ - Return SQLAlchemy models directly - use response schemas
214
+ - Use `*` imports - explicit imports only
215
+ - Catch generic `Exception` - catch specific exceptions
216
+
217
+ ## Do
218
+
219
+ - Use async/await consistently
220
+ - Add OpenAPI descriptions to all endpoints
221
+ - Use dependency injection for services
222
+ - Write tests for all endpoints
223
+ - Use Alembic for all schema changes
224
+ - Add proper logging with structlog
225
+ - Use HTTPException for API errors
226
+ - Validate all inputs with Pydantic
227
+
228
+ ## Environment Variables
229
+
230
+ ```bash
231
+ DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/dbname
232
+ SECRET_KEY=your-secret-key
233
+ REDIS_URL=redis://localhost:6379/0
234
+ DEBUG=false
235
+ ```
236
+
237
+ Always use `pydantic-settings`:
238
+
239
+ ```python
240
+ class Settings(BaseSettings):
241
+ database_url: PostgresDsn
242
+ secret_key: str
243
+ debug: bool = False
244
+
245
+ model_config = SettingsConfigDict(env_file=".env")
246
+
247
+ settings = Settings()
248
+ ```
249
+
250
+ ## Testing
251
+
252
+ ```python
253
+ # Good - async test with fixtures
254
+ @pytest.mark.asyncio
255
+ async def test_create_user(client: AsyncClient, db: AsyncSession):
256
+ response = await client.post(
257
+ "/api/users",
258
+ json={"email": "test@example.com", "password": "testpass123", "name": "Test"}
259
+ )
260
+ assert response.status_code == 201
261
+ data = response.json()
262
+ assert data["email"] == "test@example.com"
263
+ assert "password" not in data # Never return password
264
+
265
+ # conftest.py
266
+ @pytest.fixture
267
+ async def client(app: FastAPI) -> AsyncGenerator[AsyncClient, None]:
268
+ async with AsyncClient(app=app, base_url="http://test") as client:
269
+ yield client
270
+ ```