@martinffx/atelier 2.0.0 → 3.0.1
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/CHANGELOG.md +21 -0
- package/README.md +94 -253
- package/package.json +1 -1
- package/skills/{spec-orchestrator → atelier-orchestrator}/SKILL.md +3 -3
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +6 -6
- package/skills/spec-brainstorm/SKILL.md +5 -5
- package/skills/spec-implement/SKILL.md +2 -3
- package/skills/spec-plan/SKILL.md +21 -29
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/python-architecture/SKILL.md +0 -148
- package/skills/python-architecture/references/data-modeling.md +0 -558
- package/skills/python-architecture/references/ddd.md +0 -569
- package/skills/python-architecture/references/functional-core.md +0 -337
- package/skills/python-build-tools/SKILL.md +0 -153
- package/skills/python-build-tools/references/basedpyright.md +0 -321
- package/skills/python-build-tools/references/ruff.md +0 -225
- package/skills/python-build-tools/references/uv.md +0 -257
- package/skills/python-fastapi/SKILL.md +0 -184
- package/skills/python-fastapi/references/api-design.md +0 -563
- package/skills/python-fastapi/references/dependencies.md +0 -479
- package/skills/python-fastapi/references/middleware.md +0 -475
- package/skills/python-fastapi/references/validation.md +0 -497
- package/skills/python-modern-python/SKILL.md +0 -201
- package/skills/python-modern-python/references/async.md +0 -76
- package/skills/python-modern-python/references/pattern-matching.md +0 -89
- package/skills/python-modern-python/references/typing.md +0 -96
- package/skills/python-monorepo/SKILL.md +0 -146
- package/skills/python-monorepo/references/docker.md +0 -515
- package/skills/python-monorepo/references/namespace-packages.md +0 -426
- package/skills/python-monorepo/references/workspace-config.md +0 -341
- package/skills/python-sqlalchemy/SKILL.md +0 -119
- package/skills/python-sqlalchemy/references/async.md +0 -51
- package/skills/python-sqlalchemy/references/models.md +0 -67
- package/skills/python-sqlalchemy/references/queries.md +0 -57
- package/skills/python-temporal/SKILL.md +0 -128
- package/skills/python-temporal/references/patterns.md +0 -87
- package/skills/python-temporal/references/testing.md +0 -54
- package/skills/python-testing/SKILL.md +0 -161
- package/skills/python-testing/references/boundaries.md +0 -449
- package/skills/python-testing/references/mocking.md +0 -449
- package/skills/python-testing/references/pytest.md +0 -524
- package/skills/typescript-api-design/SKILL.md +0 -396
- package/skills/typescript-api-design/references/error-responses.md +0 -469
- package/skills/typescript-build-tools/SKILL.md +0 -447
- package/skills/typescript-build-tools/references/biome.md +0 -436
- package/skills/typescript-build-tools/references/bun.md +0 -432
- package/skills/typescript-build-tools/references/turborepo.md +0 -426
- package/skills/typescript-build-tools/references/vitest.md +0 -518
- package/skills/typescript-drizzle-orm/SKILL.md +0 -398
- package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
- package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
- package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
- package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
- package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
- package/skills/typescript-effect-ts/SKILL.md +0 -341
- package/skills/typescript-effect-ts/references/core.md +0 -412
- package/skills/typescript-effect-ts/references/error-handling.md +0 -468
- package/skills/typescript-effect-ts/references/resources.md +0 -476
- package/skills/typescript-effect-ts/references/schema.md +0 -536
- package/skills/typescript-effect-ts/references/services.md +0 -461
- package/skills/typescript-fastify/SKILL.md +0 -405
- package/skills/typescript-fastify/references/plugins.md +0 -329
- package/skills/typescript-fastify/references/typeid.md +0 -315
- package/skills/typescript-functional-patterns/SKILL.md +0 -402
- package/skills/typescript-functional-patterns/references/adts.md +0 -497
- package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
- package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
- package/skills/typescript-functional-patterns/references/option-result.md +0 -585
- package/skills/typescript-testing/SKILL.md +0 -353
- package/skills/typescript-testing/references/mocking.md +0 -675
- package/skills/typescript-testing/references/msw.md +0 -852
- package/skills/typescript-testing/references/snapshot-testing.md +0 -774
|
@@ -1,563 +0,0 @@
|
|
|
1
|
-
# REST API Design Patterns
|
|
2
|
-
|
|
3
|
-
Best practices for designing clean, consistent RESTful APIs with FastAPI.
|
|
4
|
-
|
|
5
|
-
## Resource Naming
|
|
6
|
-
|
|
7
|
-
### Use Nouns, Not Verbs
|
|
8
|
-
|
|
9
|
-
```python
|
|
10
|
-
# ✅ Good: Resource nouns
|
|
11
|
-
GET /users
|
|
12
|
-
POST /users
|
|
13
|
-
GET /users/{id}
|
|
14
|
-
PUT /users/{id}
|
|
15
|
-
DELETE /users/{id}
|
|
16
|
-
|
|
17
|
-
# ❌ Bad: Action verbs
|
|
18
|
-
GET /get-users
|
|
19
|
-
POST /create-user
|
|
20
|
-
GET /get-user-by-id/{id}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### Plural Names for Collections
|
|
24
|
-
|
|
25
|
-
```python
|
|
26
|
-
# ✅ Good: Plural for collections
|
|
27
|
-
GET /users # List all users
|
|
28
|
-
GET /products # List all products
|
|
29
|
-
GET /orders # List all orders
|
|
30
|
-
|
|
31
|
-
# ❌ Bad: Singular
|
|
32
|
-
GET /user
|
|
33
|
-
GET /product
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Nested Resources
|
|
37
|
-
|
|
38
|
-
```python
|
|
39
|
-
# User's orders
|
|
40
|
-
GET /users/{user_id}/orders
|
|
41
|
-
POST /users/{user_id}/orders
|
|
42
|
-
GET /users/{user_id}/orders/{order_id}
|
|
43
|
-
|
|
44
|
-
# Product reviews
|
|
45
|
-
GET /products/{product_id}/reviews
|
|
46
|
-
POST /products/{product_id}/reviews/{review_id}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## HTTP Methods
|
|
50
|
-
|
|
51
|
-
### CRUD Operations
|
|
52
|
-
|
|
53
|
-
```python
|
|
54
|
-
# Create
|
|
55
|
-
POST /users
|
|
56
|
-
{
|
|
57
|
-
"name": "Alice",
|
|
58
|
-
"email": "alice@example.com"
|
|
59
|
-
}
|
|
60
|
-
# Returns: 201 Created with resource
|
|
61
|
-
|
|
62
|
-
# Read (list)
|
|
63
|
-
GET /users
|
|
64
|
-
# Returns: 200 OK with array
|
|
65
|
-
|
|
66
|
-
# Read (single)
|
|
67
|
-
GET /users/{id}
|
|
68
|
-
# Returns: 200 OK with object
|
|
69
|
-
|
|
70
|
-
# Update (full replacement)
|
|
71
|
-
PUT /users/{id}
|
|
72
|
-
{
|
|
73
|
-
"name": "Alice Updated",
|
|
74
|
-
"email": "alice.updated@example.com"
|
|
75
|
-
}
|
|
76
|
-
# Returns: 200 OK with updated resource
|
|
77
|
-
|
|
78
|
-
# Update (partial)
|
|
79
|
-
PATCH /users/{id}
|
|
80
|
-
{
|
|
81
|
-
"name": "Alice Updated"
|
|
82
|
-
}
|
|
83
|
-
# Returns: 200 OK with updated resource
|
|
84
|
-
|
|
85
|
-
# Delete
|
|
86
|
-
DELETE /users/{id}
|
|
87
|
-
# Returns: 204 No Content
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Method Semantics
|
|
91
|
-
|
|
92
|
-
**GET** - Read, no side effects, cacheable
|
|
93
|
-
- `GET /users` - List users
|
|
94
|
-
- `GET /users/{id}` - Get user
|
|
95
|
-
|
|
96
|
-
**POST** - Create resource, non-idempotent
|
|
97
|
-
- `POST /users` - Create user
|
|
98
|
-
- `POST /orders` - Create order
|
|
99
|
-
|
|
100
|
-
**PUT** - Replace resource, idempotent
|
|
101
|
-
- `PUT /users/{id}` - Replace user entirely
|
|
102
|
-
|
|
103
|
-
**PATCH** - Partial update, idempotent
|
|
104
|
-
- `PATCH /users/{id}` - Update specific fields
|
|
105
|
-
|
|
106
|
-
**DELETE** - Delete resource, idempotent
|
|
107
|
-
- `DELETE /users/{id}` - Delete user
|
|
108
|
-
|
|
109
|
-
## Status Codes
|
|
110
|
-
|
|
111
|
-
### Success Codes
|
|
112
|
-
|
|
113
|
-
```python
|
|
114
|
-
from fastapi import status
|
|
115
|
-
|
|
116
|
-
# 200 OK - Success with body
|
|
117
|
-
@app.get("/users/{id}")
|
|
118
|
-
def get_user(id: int):
|
|
119
|
-
...
|
|
120
|
-
|
|
121
|
-
# 201 Created - Resource created
|
|
122
|
-
@app.post("/users", status_code=status.HTTP_201_CREATED)
|
|
123
|
-
def create_user(user: CreateUserRequest):
|
|
124
|
-
...
|
|
125
|
-
|
|
126
|
-
# 204 No Content - Success without body
|
|
127
|
-
@app.delete("/users/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
128
|
-
def delete_user(id: int):
|
|
129
|
-
...
|
|
130
|
-
|
|
131
|
-
# 202 Accepted - Async operation accepted
|
|
132
|
-
@app.post("/reports", status_code=status.HTTP_202_ACCEPTED)
|
|
133
|
-
def generate_report(report: ReportRequest):
|
|
134
|
-
"""Report generation started"""
|
|
135
|
-
...
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Client Error Codes
|
|
139
|
-
|
|
140
|
-
```python
|
|
141
|
-
from fastapi import HTTPException
|
|
142
|
-
|
|
143
|
-
# 400 Bad Request - Invalid input
|
|
144
|
-
if not valid_email(email):
|
|
145
|
-
raise HTTPException(
|
|
146
|
-
status_code=400,
|
|
147
|
-
detail="Invalid email format",
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# 401 Unauthorized - Not authenticated
|
|
151
|
-
if not token:
|
|
152
|
-
raise HTTPException(
|
|
153
|
-
status_code=401,
|
|
154
|
-
detail="Authentication required",
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# 403 Forbidden - Authenticated but not authorized
|
|
158
|
-
if user.role != "admin":
|
|
159
|
-
raise HTTPException(
|
|
160
|
-
status_code=403,
|
|
161
|
-
detail="Admin access required",
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# 404 Not Found - Resource doesn't exist
|
|
165
|
-
user = db.get(user_id)
|
|
166
|
-
if not user:
|
|
167
|
-
raise HTTPException(
|
|
168
|
-
status_code=404,
|
|
169
|
-
detail="User not found",
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
# 409 Conflict - Resource conflict
|
|
173
|
-
if user_exists(email):
|
|
174
|
-
raise HTTPException(
|
|
175
|
-
status_code=409,
|
|
176
|
-
detail="User with this email already exists",
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
# 422 Unprocessable Entity - Validation error (Pydantic automatic)
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
### Server Error Codes
|
|
183
|
-
|
|
184
|
-
```python
|
|
185
|
-
# 500 Internal Server Error - Unexpected error
|
|
186
|
-
try:
|
|
187
|
-
result = process_data(data)
|
|
188
|
-
except Exception as e:
|
|
189
|
-
raise HTTPException(
|
|
190
|
-
status_code=500,
|
|
191
|
-
detail="Internal server error",
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
# 503 Service Unavailable - Service temporarily down
|
|
195
|
-
if not db.is_connected():
|
|
196
|
-
raise HTTPException(
|
|
197
|
-
status_code=503,
|
|
198
|
-
detail="Database unavailable",
|
|
199
|
-
)
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Error Responses
|
|
203
|
-
|
|
204
|
-
### RFC 7807 Problem Details
|
|
205
|
-
|
|
206
|
-
```python
|
|
207
|
-
from pydantic import BaseModel
|
|
208
|
-
|
|
209
|
-
class ProblemDetail(BaseModel):
|
|
210
|
-
"""RFC 7807 Problem Details"""
|
|
211
|
-
type: str # URI reference to error type
|
|
212
|
-
title: str # Human-readable summary
|
|
213
|
-
status: int # HTTP status code
|
|
214
|
-
detail: str # Human-readable explanation
|
|
215
|
-
instance: str | None = None # URI reference to specific occurrence
|
|
216
|
-
|
|
217
|
-
@app.exception_handler(BusinessError)
|
|
218
|
-
async def business_error_handler(request: Request, exc: BusinessError):
|
|
219
|
-
return JSONResponse(
|
|
220
|
-
status_code=400,
|
|
221
|
-
content=ProblemDetail(
|
|
222
|
-
type="https://api.example.com/errors/business-error",
|
|
223
|
-
title="Business Rule Violation",
|
|
224
|
-
status=400,
|
|
225
|
-
detail=exc.message,
|
|
226
|
-
instance=request.url.path,
|
|
227
|
-
).model_dump(),
|
|
228
|
-
)
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### Validation Errors
|
|
232
|
-
|
|
233
|
-
```python
|
|
234
|
-
from fastapi.exceptions import RequestValidationError
|
|
235
|
-
|
|
236
|
-
@app.exception_handler(RequestValidationError)
|
|
237
|
-
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
238
|
-
"""Structured validation errors"""
|
|
239
|
-
errors = []
|
|
240
|
-
for error in exc.errors():
|
|
241
|
-
errors.append({
|
|
242
|
-
"field": ".".join(str(loc) for loc in error["loc"][1:]), # Skip 'body'
|
|
243
|
-
"message": error["msg"],
|
|
244
|
-
"type": error["type"],
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
return JSONResponse(
|
|
248
|
-
status_code=422,
|
|
249
|
-
content={
|
|
250
|
-
"type": "https://api.example.com/errors/validation",
|
|
251
|
-
"title": "Validation Error",
|
|
252
|
-
"status": 422,
|
|
253
|
-
"errors": errors,
|
|
254
|
-
},
|
|
255
|
-
)
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
## Pagination
|
|
259
|
-
|
|
260
|
-
### Offset-Based Pagination
|
|
261
|
-
|
|
262
|
-
```python
|
|
263
|
-
from pydantic import BaseModel
|
|
264
|
-
|
|
265
|
-
class PaginatedResponse(BaseModel):
|
|
266
|
-
items: list
|
|
267
|
-
total: int
|
|
268
|
-
page: int
|
|
269
|
-
page_size: int
|
|
270
|
-
pages: int
|
|
271
|
-
|
|
272
|
-
@app.get("/users")
|
|
273
|
-
def list_users(
|
|
274
|
-
page: int = Query(default=1, ge=1),
|
|
275
|
-
page_size: int = Query(default=10, ge=1, le=100),
|
|
276
|
-
) -> PaginatedResponse:
|
|
277
|
-
"""Offset pagination"""
|
|
278
|
-
skip = (page - 1) * page_size
|
|
279
|
-
limit = page_size
|
|
280
|
-
|
|
281
|
-
users = db.query(User).offset(skip).limit(limit).all()
|
|
282
|
-
total = db.query(User).count()
|
|
283
|
-
pages = (total + page_size - 1) // page_size
|
|
284
|
-
|
|
285
|
-
return PaginatedResponse(
|
|
286
|
-
items=[UserResponse.model_validate(u) for u in users],
|
|
287
|
-
total=total,
|
|
288
|
-
page=page,
|
|
289
|
-
page_size=page_size,
|
|
290
|
-
pages=pages,
|
|
291
|
-
)
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### Cursor-Based Pagination
|
|
295
|
-
|
|
296
|
-
Better for large datasets:
|
|
297
|
-
|
|
298
|
-
```python
|
|
299
|
-
class CursorPaginatedResponse(BaseModel):
|
|
300
|
-
items: list
|
|
301
|
-
next_cursor: str | None
|
|
302
|
-
has_more: bool
|
|
303
|
-
|
|
304
|
-
@app.get("/users")
|
|
305
|
-
def list_users(
|
|
306
|
-
cursor: str | None = None,
|
|
307
|
-
limit: int = Query(default=10, ge=1, le=100),
|
|
308
|
-
) -> CursorPaginatedResponse:
|
|
309
|
-
"""Cursor pagination"""
|
|
310
|
-
query = db.query(User)
|
|
311
|
-
|
|
312
|
-
if cursor:
|
|
313
|
-
# Decode cursor (e.g., base64 encoded ID)
|
|
314
|
-
cursor_id = decode_cursor(cursor)
|
|
315
|
-
query = query.filter(User.id > cursor_id)
|
|
316
|
-
|
|
317
|
-
users = query.limit(limit + 1).all()
|
|
318
|
-
has_more = len(users) > limit
|
|
319
|
-
items = users[:limit]
|
|
320
|
-
|
|
321
|
-
next_cursor = None
|
|
322
|
-
if has_more:
|
|
323
|
-
next_cursor = encode_cursor(items[-1].id)
|
|
324
|
-
|
|
325
|
-
return CursorPaginatedResponse(
|
|
326
|
-
items=[UserResponse.model_validate(u) for u in items],
|
|
327
|
-
next_cursor=next_cursor,
|
|
328
|
-
has_more=has_more,
|
|
329
|
-
)
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
## Filtering and Sorting
|
|
333
|
-
|
|
334
|
-
```python
|
|
335
|
-
from typing import Literal
|
|
336
|
-
|
|
337
|
-
@app.get("/products")
|
|
338
|
-
def list_products(
|
|
339
|
-
category: str | None = None,
|
|
340
|
-
min_price: float | None = None,
|
|
341
|
-
max_price: float | None = None,
|
|
342
|
-
sort: Literal["price", "name", "created_at"] = "created_at",
|
|
343
|
-
order: Literal["asc", "desc"] = "desc",
|
|
344
|
-
):
|
|
345
|
-
"""Filtering and sorting"""
|
|
346
|
-
query = db.query(Product)
|
|
347
|
-
|
|
348
|
-
# Filtering
|
|
349
|
-
if category:
|
|
350
|
-
query = query.filter(Product.category == category)
|
|
351
|
-
if min_price:
|
|
352
|
-
query = query.filter(Product.price >= min_price)
|
|
353
|
-
if max_price:
|
|
354
|
-
query = query.filter(Product.price <= max_price)
|
|
355
|
-
|
|
356
|
-
# Sorting
|
|
357
|
-
sort_column = getattr(Product, sort)
|
|
358
|
-
if order == "asc":
|
|
359
|
-
query = query.order_by(sort_column.asc())
|
|
360
|
-
else:
|
|
361
|
-
query = query.order_by(sort_column.desc())
|
|
362
|
-
|
|
363
|
-
return query.all()
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
## Versioning
|
|
367
|
-
|
|
368
|
-
### URL Versioning
|
|
369
|
-
|
|
370
|
-
```python
|
|
371
|
-
from fastapi import APIRouter
|
|
372
|
-
|
|
373
|
-
# Version 1
|
|
374
|
-
v1_router = APIRouter(prefix="/v1")
|
|
375
|
-
|
|
376
|
-
@v1_router.get("/users")
|
|
377
|
-
def list_users_v1():
|
|
378
|
-
"""V1 implementation"""
|
|
379
|
-
...
|
|
380
|
-
|
|
381
|
-
# Version 2
|
|
382
|
-
v2_router = APIRouter(prefix="/v2")
|
|
383
|
-
|
|
384
|
-
@v2_router.get("/users")
|
|
385
|
-
def list_users_v2():
|
|
386
|
-
"""V2 implementation with new fields"""
|
|
387
|
-
...
|
|
388
|
-
|
|
389
|
-
app.include_router(v1_router)
|
|
390
|
-
app.include_router(v2_router)
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
### Header Versioning
|
|
394
|
-
|
|
395
|
-
```python
|
|
396
|
-
from fastapi import Header
|
|
397
|
-
|
|
398
|
-
@app.get("/users")
|
|
399
|
-
def list_users(accept_version: str = Header(default="v1", alias="Accept-Version")):
|
|
400
|
-
"""Version via header"""
|
|
401
|
-
if accept_version == "v2":
|
|
402
|
-
return list_users_v2()
|
|
403
|
-
return list_users_v1()
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
## HATEOAS (Hypermedia)
|
|
407
|
-
|
|
408
|
-
Include links to related resources:
|
|
409
|
-
|
|
410
|
-
```python
|
|
411
|
-
class UserResponse(BaseModel):
|
|
412
|
-
id: int
|
|
413
|
-
name: str
|
|
414
|
-
email: str
|
|
415
|
-
links: dict[str, str]
|
|
416
|
-
|
|
417
|
-
@app.get("/users/{id}")
|
|
418
|
-
def get_user(id: int) -> UserResponse:
|
|
419
|
-
user = db.get(User, id)
|
|
420
|
-
|
|
421
|
-
return UserResponse(
|
|
422
|
-
id=user.id,
|
|
423
|
-
name=user.name,
|
|
424
|
-
email=user.email,
|
|
425
|
-
links={
|
|
426
|
-
"self": f"/users/{user.id}",
|
|
427
|
-
"orders": f"/users/{user.id}/orders",
|
|
428
|
-
"update": f"/users/{user.id}",
|
|
429
|
-
"delete": f"/users/{user.id}",
|
|
430
|
-
},
|
|
431
|
-
)
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
## Idempotency
|
|
435
|
-
|
|
436
|
-
Ensure same request can be safely retried:
|
|
437
|
-
|
|
438
|
-
```python
|
|
439
|
-
from fastapi import Header
|
|
440
|
-
import hashlib
|
|
441
|
-
|
|
442
|
-
# Idempotency-Key header for POST requests
|
|
443
|
-
@app.post("/orders")
|
|
444
|
-
def create_order(
|
|
445
|
-
order: CreateOrderRequest,
|
|
446
|
-
idempotency_key: str = Header(),
|
|
447
|
-
db: Session = Depends(get_db),
|
|
448
|
-
):
|
|
449
|
-
"""Idempotent order creation"""
|
|
450
|
-
# Check if order with this key exists
|
|
451
|
-
existing = db.query(Order).filter(Order.idempotency_key == idempotency_key).first()
|
|
452
|
-
|
|
453
|
-
if existing:
|
|
454
|
-
# Return existing order instead of creating duplicate
|
|
455
|
-
return existing
|
|
456
|
-
|
|
457
|
-
# Create new order
|
|
458
|
-
new_order = Order(**order.model_dump(), idempotency_key=idempotency_key)
|
|
459
|
-
db.add(new_order)
|
|
460
|
-
db.commit()
|
|
461
|
-
|
|
462
|
-
return new_order
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
## Rate Limiting
|
|
466
|
-
|
|
467
|
-
```python
|
|
468
|
-
from fastapi import Request
|
|
469
|
-
|
|
470
|
-
@app.get("/search")
|
|
471
|
-
async def search(
|
|
472
|
-
q: str,
|
|
473
|
-
request: Request,
|
|
474
|
-
):
|
|
475
|
-
"""Rate limited endpoint"""
|
|
476
|
-
client_ip = request.client.host
|
|
477
|
-
|
|
478
|
-
# Check rate limit
|
|
479
|
-
if is_rate_limited(client_ip):
|
|
480
|
-
raise HTTPException(
|
|
481
|
-
status_code=429,
|
|
482
|
-
detail="Rate limit exceeded",
|
|
483
|
-
headers={"Retry-After": "60"},
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
return perform_search(q)
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
## OpenAPI Customization
|
|
490
|
-
|
|
491
|
-
```python
|
|
492
|
-
from fastapi.openapi.utils import get_openapi
|
|
493
|
-
|
|
494
|
-
def custom_openapi():
|
|
495
|
-
if app.openapi_schema:
|
|
496
|
-
return app.openapi_schema
|
|
497
|
-
|
|
498
|
-
openapi_schema = get_openapi(
|
|
499
|
-
title="My API",
|
|
500
|
-
version="1.0.0",
|
|
501
|
-
description="API for my application",
|
|
502
|
-
routes=app.routes,
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
# Add custom fields
|
|
506
|
-
openapi_schema["info"]["x-logo"] = {
|
|
507
|
-
"url": "https://example.com/logo.png"
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
app.openapi_schema = openapi_schema
|
|
511
|
-
return app.openapi_schema
|
|
512
|
-
|
|
513
|
-
app.openapi = custom_openapi
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
## Best Practices
|
|
517
|
-
|
|
518
|
-
1. **Use standard HTTP methods**: GET, POST, PUT, PATCH, DELETE
|
|
519
|
-
2. **Plural resource names**: `/users`, not `/user`
|
|
520
|
-
3. **Consistent naming**: lowercase, kebab-case for URLs
|
|
521
|
-
4. **Appropriate status codes**: 201 for creation, 204 for deletion
|
|
522
|
-
5. **Structured errors**: RFC 7807 Problem Details
|
|
523
|
-
6. **Pagination**: Always paginate list endpoints
|
|
524
|
-
7. **Versioning**: Plan for API evolution
|
|
525
|
-
8. **Idempotency**: Safe retry for mutations
|
|
526
|
-
9. **Rate limiting**: Protect against abuse
|
|
527
|
-
10. **Documentation**: Use OpenAPI descriptions
|
|
528
|
-
|
|
529
|
-
## Anti-Patterns
|
|
530
|
-
|
|
531
|
-
❌ **Verbs in URLs**: `/createUser` instead of `POST /users`
|
|
532
|
-
❌ **Custom error formats**: Inconsistent error responses
|
|
533
|
-
❌ **No pagination**: Returning unbounded lists
|
|
534
|
-
❌ **Wrong status codes**: Returning 200 for errors
|
|
535
|
-
❌ **No versioning**: Breaking changes without version
|
|
536
|
-
❌ **Ignoring idempotency**: Duplicate requests create duplicates
|
|
537
|
-
❌ **No rate limiting**: Vulnerable to abuse
|
|
538
|
-
|
|
539
|
-
## Summary
|
|
540
|
-
|
|
541
|
-
**Resource naming:**
|
|
542
|
-
- Nouns, not verbs
|
|
543
|
-
- Plural for collections
|
|
544
|
-
- Nested for relationships
|
|
545
|
-
|
|
546
|
-
**HTTP methods:**
|
|
547
|
-
- GET (read), POST (create), PUT (replace), PATCH (update), DELETE (delete)
|
|
548
|
-
- Idempotent: GET, PUT, PATCH, DELETE
|
|
549
|
-
- Non-idempotent: POST
|
|
550
|
-
|
|
551
|
-
**Status codes:**
|
|
552
|
-
- 2xx: Success
|
|
553
|
-
- 4xx: Client error
|
|
554
|
-
- 5xx: Server error
|
|
555
|
-
|
|
556
|
-
**Pagination:**
|
|
557
|
-
- Offset-based (simple)
|
|
558
|
-
- Cursor-based (scalable)
|
|
559
|
-
|
|
560
|
-
**Error handling:**
|
|
561
|
-
- Structured errors (RFC 7807)
|
|
562
|
-
- Helpful messages
|
|
563
|
-
- Consistent format
|