red64-cli 0.1.0 → 0.3.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.
- package/README.md +1 -2
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/ListScreen.d.ts.map +1 -1
- package/dist/components/screens/ListScreen.js +28 -3
- package/dist/components/screens/ListScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +212 -13
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
- package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
- package/dist/components/ui/ArtifactsSidebar.js +51 -0
- package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
- package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
- package/dist/components/ui/FeatureSidebar.js +1 -1
- package/dist/components/ui/FeatureSidebar.js.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/services/ClaudeErrorDetector.js +3 -3
- package/dist/services/ClaudeErrorDetector.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# API Design Standards
|
|
2
|
+
|
|
3
|
+
RESTful API conventions for FastAPI applications with consistent patterns for routing, responses, and documentation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Resource-oriented**: URLs represent resources, HTTP methods represent actions
|
|
10
|
+
- **Consistent responses**: Every endpoint follows the same response envelope
|
|
11
|
+
- **Self-documenting**: OpenAPI spec generated from code, not maintained separately
|
|
12
|
+
- **Dependency injection**: Auth, DB, and shared logic injected via FastAPI dependencies
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## RESTful Conventions
|
|
17
|
+
|
|
18
|
+
### URL Structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
/api/v1/{resource} # Collection
|
|
22
|
+
/api/v1/{resource}/{id} # Single resource
|
|
23
|
+
/api/v1/{resource}/{id}/{sub} # Nested resource (max 2 levels)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### HTTP Methods
|
|
27
|
+
|
|
28
|
+
| Method | URL | Action | Status Code |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `GET` | `/users` | List users | 200 |
|
|
31
|
+
| `GET` | `/users/42` | Get single user | 200 |
|
|
32
|
+
| `POST` | `/users` | Create user | 201 |
|
|
33
|
+
| `PUT` | `/users/42` | Full replace | 200 |
|
|
34
|
+
| `PATCH` | `/users/42` | Partial update | 200 |
|
|
35
|
+
| `DELETE` | `/users/42` | Delete user | 204 |
|
|
36
|
+
|
|
37
|
+
### Naming Rules
|
|
38
|
+
|
|
39
|
+
- Plural nouns for resources: `/users`, `/posts`, `/comments`
|
|
40
|
+
- Lowercase with hyphens for multi-word: `/user-profiles`, `/api-keys`
|
|
41
|
+
- No verbs in URLs (use HTTP methods instead)
|
|
42
|
+
- No trailing slashes
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# GOOD
|
|
46
|
+
GET /api/v1/users
|
|
47
|
+
POST /api/v1/users
|
|
48
|
+
GET /api/v1/users/42/posts
|
|
49
|
+
|
|
50
|
+
# BAD
|
|
51
|
+
GET /api/v1/getUsers
|
|
52
|
+
POST /api/v1/createUser
|
|
53
|
+
GET /api/v1/user/42/getAllPosts
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Router Organization
|
|
59
|
+
|
|
60
|
+
### Feature-Based Routers
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# app/api/__init__.py
|
|
64
|
+
from fastapi import APIRouter
|
|
65
|
+
from app.api import users, posts, auth, health
|
|
66
|
+
|
|
67
|
+
api_router = APIRouter(prefix="/api/v1")
|
|
68
|
+
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
|
69
|
+
api_router.include_router(users.router, prefix="/users", tags=["Users"])
|
|
70
|
+
api_router.include_router(posts.router, prefix="/posts", tags=["Posts"])
|
|
71
|
+
api_router.include_router(health.router, prefix="/health", tags=["Health"])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Single Resource Router
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# app/api/users.py
|
|
78
|
+
from fastapi import APIRouter, Depends, status
|
|
79
|
+
from app.dependencies import get_db, get_current_user
|
|
80
|
+
|
|
81
|
+
router = APIRouter()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.get("", response_model=PaginatedResponse[UserResponse])
|
|
85
|
+
async def list_users(
|
|
86
|
+
page: int = Query(default=1, ge=1),
|
|
87
|
+
per_page: int = Query(default=20, ge=1, le=100),
|
|
88
|
+
db: AsyncSession = Depends(get_db),
|
|
89
|
+
current_user: User = Depends(get_current_user),
|
|
90
|
+
):
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@router.get("/{user_id}", response_model=UserResponse)
|
|
95
|
+
async def get_user(
|
|
96
|
+
user_id: int = Path(ge=1),
|
|
97
|
+
db: AsyncSession = Depends(get_db),
|
|
98
|
+
):
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
|
103
|
+
async def create_user(
|
|
104
|
+
data: CreateUserRequest,
|
|
105
|
+
db: AsyncSession = Depends(get_db),
|
|
106
|
+
):
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@router.patch("/{user_id}", response_model=UserResponse)
|
|
111
|
+
async def update_user(
|
|
112
|
+
user_id: int = Path(ge=1),
|
|
113
|
+
data: UpdateUserRequest,
|
|
114
|
+
db: AsyncSession = Depends(get_db),
|
|
115
|
+
current_user: User = Depends(get_current_user),
|
|
116
|
+
):
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
121
|
+
async def delete_user(
|
|
122
|
+
user_id: int = Path(ge=1),
|
|
123
|
+
db: AsyncSession = Depends(get_db),
|
|
124
|
+
current_user: User = Depends(get_current_user),
|
|
125
|
+
):
|
|
126
|
+
...
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## API Versioning
|
|
132
|
+
|
|
133
|
+
### URL Path Versioning (Recommended)
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# app/main.py
|
|
137
|
+
from app.api import api_router # /api/v1/...
|
|
138
|
+
|
|
139
|
+
app = FastAPI(title="My API", version="1.0.0")
|
|
140
|
+
app.include_router(api_router)
|
|
141
|
+
|
|
142
|
+
# When v2 is needed:
|
|
143
|
+
# from app.api_v2 import api_router_v2 # /api/v2/...
|
|
144
|
+
# app.include_router(api_router_v2)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Versioning Strategy
|
|
148
|
+
|
|
149
|
+
| Approach | Pros | Cons |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| URL path (`/api/v1/`) | Explicit, easy to route | URL changes on version bump |
|
|
152
|
+
| Header (`Accept-Version: 1`) | Clean URLs | Harder to test, less discoverable |
|
|
153
|
+
| Query param (`?version=1`) | Simple | Clutters query string |
|
|
154
|
+
|
|
155
|
+
**Decision**: Use URL path versioning. It is explicit and works with every HTTP client and caching layer.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Pagination
|
|
160
|
+
|
|
161
|
+
### Cursor-Based (Default for Lists)
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from pydantic import BaseModel
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
168
|
+
items: list[T]
|
|
169
|
+
has_next: bool
|
|
170
|
+
next_cursor: str | None = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@router.get("/posts", response_model=PaginatedResponse[PostResponse])
|
|
174
|
+
async def list_posts(
|
|
175
|
+
cursor: str | None = Query(default=None, description="Pagination cursor"),
|
|
176
|
+
limit: int = Query(default=20, ge=1, le=100),
|
|
177
|
+
db: AsyncSession = Depends(get_db),
|
|
178
|
+
):
|
|
179
|
+
posts, has_next, next_cursor = await post_service.list_posts(
|
|
180
|
+
cursor=cursor, limit=limit,
|
|
181
|
+
)
|
|
182
|
+
return PaginatedResponse(
|
|
183
|
+
items=[PostResponse.model_validate(p) for p in posts],
|
|
184
|
+
has_next=has_next,
|
|
185
|
+
next_cursor=next_cursor,
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Offset-Based (Simple Admin UIs)
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
class PagedResponse(BaseModel, Generic[T]):
|
|
193
|
+
items: list[T]
|
|
194
|
+
total: int
|
|
195
|
+
page: int
|
|
196
|
+
per_page: int
|
|
197
|
+
total_pages: int
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@router.get("/admin/users", response_model=PagedResponse[UserResponse])
|
|
201
|
+
async def list_users_admin(
|
|
202
|
+
page: int = Query(default=1, ge=1),
|
|
203
|
+
per_page: int = Query(default=20, ge=1, le=100),
|
|
204
|
+
db: AsyncSession = Depends(get_db),
|
|
205
|
+
):
|
|
206
|
+
users, total = await user_service.list_users_paged(page=page, per_page=per_page)
|
|
207
|
+
return PagedResponse(
|
|
208
|
+
items=[UserResponse.model_validate(u) for u in users],
|
|
209
|
+
total=total,
|
|
210
|
+
page=page,
|
|
211
|
+
per_page=per_page,
|
|
212
|
+
total_pages=(total + per_page - 1) // per_page,
|
|
213
|
+
)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### When to Use Each
|
|
217
|
+
|
|
218
|
+
| Type | Use Case |
|
|
219
|
+
|---|---|
|
|
220
|
+
| Cursor-based | Public APIs, infinite scroll, real-time data |
|
|
221
|
+
| Offset-based | Admin panels, dashboards, static lists |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Filtering and Sorting
|
|
226
|
+
|
|
227
|
+
### Query Parameter Patterns
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
@router.get("/posts", response_model=PaginatedResponse[PostResponse])
|
|
231
|
+
async def list_posts(
|
|
232
|
+
status: str | None = Query(default=None, pattern="^(draft|published|archived)$"),
|
|
233
|
+
author_id: int | None = Query(default=None, ge=1),
|
|
234
|
+
search: str | None = Query(default=None, min_length=1, max_length=200),
|
|
235
|
+
sort: str = Query(default="-created_at", pattern="^-?(created_at|title|updated_at)$"),
|
|
236
|
+
cursor: str | None = Query(default=None),
|
|
237
|
+
limit: int = Query(default=20, ge=1, le=100),
|
|
238
|
+
db: AsyncSession = Depends(get_db),
|
|
239
|
+
):
|
|
240
|
+
...
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Sort Convention
|
|
244
|
+
|
|
245
|
+
- Prefix with `-` for descending: `-created_at`
|
|
246
|
+
- No prefix for ascending: `title`
|
|
247
|
+
- Validate against allowed fields to prevent SQL injection
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## HTTP Status Codes
|
|
252
|
+
|
|
253
|
+
### Standard Responses
|
|
254
|
+
|
|
255
|
+
| Code | Meaning | When to Use |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| `200` | OK | Successful GET, PUT, PATCH |
|
|
258
|
+
| `201` | Created | Successful POST that creates a resource |
|
|
259
|
+
| `204` | No Content | Successful DELETE |
|
|
260
|
+
| `400` | Bad Request | Malformed request syntax |
|
|
261
|
+
| `401` | Unauthorized | Missing or invalid authentication |
|
|
262
|
+
| `403` | Forbidden | Authenticated but insufficient permissions |
|
|
263
|
+
| `404` | Not Found | Resource does not exist |
|
|
264
|
+
| `409` | Conflict | Duplicate resource, state conflict |
|
|
265
|
+
| `422` | Unprocessable Entity | Validation errors (Pydantic default) |
|
|
266
|
+
| `429` | Too Many Requests | Rate limit exceeded |
|
|
267
|
+
| `500` | Internal Server Error | Unhandled server error |
|
|
268
|
+
| `502` | Bad Gateway | External service failure |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Response Envelope
|
|
273
|
+
|
|
274
|
+
### Success Response
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"id": 42,
|
|
279
|
+
"email": "user@example.com",
|
|
280
|
+
"name": "Jane Doe",
|
|
281
|
+
"created_at": "2024-01-15T10:30:00Z"
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
For single resources, return the object directly (no wrapping). FastAPI + Pydantic handles serialization.
|
|
286
|
+
|
|
287
|
+
### Collection Response
|
|
288
|
+
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"items": [...],
|
|
292
|
+
"has_next": true,
|
|
293
|
+
"next_cursor": "eyJpZCI6IDQyfQ=="
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Error Response
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"error": {
|
|
302
|
+
"code": "NOT_FOUND",
|
|
303
|
+
"message": "User not found: 42",
|
|
304
|
+
"details": {"resource": "User", "identifier": "42"}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Rate Limiting
|
|
312
|
+
|
|
313
|
+
### Per-Endpoint Limits
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
from slowapi import Limiter
|
|
317
|
+
from slowapi.util import get_remote_address
|
|
318
|
+
|
|
319
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
320
|
+
|
|
321
|
+
@router.post("/auth/login")
|
|
322
|
+
@limiter.limit("5/minute")
|
|
323
|
+
async def login(request: Request, data: LoginRequest):
|
|
324
|
+
...
|
|
325
|
+
|
|
326
|
+
@router.get("/search")
|
|
327
|
+
@limiter.limit("30/minute")
|
|
328
|
+
async def search(request: Request, q: str = Query(...)):
|
|
329
|
+
...
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Rate Limit Headers
|
|
333
|
+
|
|
334
|
+
Include in responses so clients can self-regulate:
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
X-RateLimit-Limit: 30
|
|
338
|
+
X-RateLimit-Remaining: 28
|
|
339
|
+
X-RateLimit-Reset: 1705312800
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Dependency Injection
|
|
345
|
+
|
|
346
|
+
### Common Dependencies
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
# app/dependencies.py
|
|
350
|
+
from collections.abc import AsyncIterator
|
|
351
|
+
from fastapi import Depends
|
|
352
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
async def get_db() -> AsyncIterator[AsyncSession]:
|
|
356
|
+
async with async_session() as session:
|
|
357
|
+
yield session
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def get_current_user(
|
|
361
|
+
token: str = Depends(oauth2_scheme),
|
|
362
|
+
db: AsyncSession = Depends(get_db),
|
|
363
|
+
) -> User:
|
|
364
|
+
...
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def require_role(*roles: str):
|
|
368
|
+
async def dependency(user: User = Depends(get_current_user)) -> User:
|
|
369
|
+
if user.role not in roles:
|
|
370
|
+
raise HTTPException(status_code=403)
|
|
371
|
+
return user
|
|
372
|
+
return dependency
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Service Injection Pattern
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
async def get_user_service(db: AsyncSession = Depends(get_db)) -> UserService:
|
|
379
|
+
return UserService(repo=UserRepo(db))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@router.post("", status_code=201)
|
|
383
|
+
async def create_user(
|
|
384
|
+
data: CreateUserRequest,
|
|
385
|
+
service: UserService = Depends(get_user_service),
|
|
386
|
+
) -> UserResponse:
|
|
387
|
+
user = await service.create_user(data)
|
|
388
|
+
return UserResponse.model_validate(user)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## OpenAPI Documentation
|
|
394
|
+
|
|
395
|
+
### Customize FastAPI Docs
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
app = FastAPI(
|
|
399
|
+
title="My API",
|
|
400
|
+
version="1.0.0",
|
|
401
|
+
description="API for managing users and content",
|
|
402
|
+
docs_url="/docs", # Swagger UI
|
|
403
|
+
redoc_url="/redoc", # ReDoc
|
|
404
|
+
openapi_url="/openapi.json",
|
|
405
|
+
)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Document Endpoint Responses
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
@router.get(
|
|
412
|
+
"/{user_id}",
|
|
413
|
+
response_model=UserResponse,
|
|
414
|
+
summary="Get user by ID",
|
|
415
|
+
description="Retrieve a single user by their unique identifier.",
|
|
416
|
+
responses={
|
|
417
|
+
404: {"model": ErrorResponse, "description": "User not found"},
|
|
418
|
+
401: {"model": ErrorResponse, "description": "Not authenticated"},
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
async def get_user(user_id: int = Path(ge=1)) -> UserResponse:
|
|
422
|
+
...
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Health Check
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
# app/api/health.py
|
|
431
|
+
from fastapi import APIRouter
|
|
432
|
+
|
|
433
|
+
router = APIRouter()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@router.get("")
|
|
437
|
+
async def health():
|
|
438
|
+
return {"status": "ok"}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@router.get("/ready")
|
|
442
|
+
async def readiness(db: AsyncSession = Depends(get_db)):
|
|
443
|
+
try:
|
|
444
|
+
await db.execute(text("SELECT 1"))
|
|
445
|
+
return {"status": "ready", "database": "connected"}
|
|
446
|
+
except Exception:
|
|
447
|
+
return JSONResponse(
|
|
448
|
+
status_code=503,
|
|
449
|
+
content={"status": "not_ready", "database": "disconnected"},
|
|
450
|
+
)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Anti-Patterns
|
|
456
|
+
|
|
457
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
458
|
+
|---|---|---|
|
|
459
|
+
| Verbs in URLs | Not RESTful | Use HTTP methods |
|
|
460
|
+
| Inconsistent status codes | Confusing for clients | Follow the table above |
|
|
461
|
+
| No pagination | Unbounded responses | Always paginate collections |
|
|
462
|
+
| Business logic in routes | Hard to test | Inject services via dependencies |
|
|
463
|
+
| No API versioning | Breaking changes break clients | Version from day one |
|
|
464
|
+
| Returning `dict` | No validation, no docs | Use Pydantic response models |
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
_APIs are contracts. Be consistent, be explicit, and let the framework generate documentation from your code._
|