@malamute/ai-rules 1.0.0 → 1.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.
Files changed (145) hide show
  1. package/README.md +272 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/CLAUDE.md +52 -149
  4. package/configs/_shared/rules/conventions/documentation.md +324 -0
  5. package/configs/_shared/rules/conventions/git.md +265 -0
  6. package/configs/_shared/rules/conventions/npm.md +80 -0
  7. package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
  8. package/configs/_shared/rules/conventions/principles.md +334 -0
  9. package/configs/_shared/rules/devops/ci-cd.md +262 -0
  10. package/configs/_shared/rules/devops/docker.md +275 -0
  11. package/configs/_shared/rules/devops/nx.md +194 -0
  12. package/configs/_shared/rules/domain/backend/api-design.md +203 -0
  13. package/configs/_shared/rules/lang/csharp/async.md +220 -0
  14. package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
  15. package/configs/_shared/rules/lang/csharp/linq.md +210 -0
  16. package/configs/_shared/rules/lang/python/async.md +337 -0
  17. package/configs/_shared/rules/lang/python/celery.md +476 -0
  18. package/configs/_shared/rules/lang/python/config.md +339 -0
  19. package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
  20. package/configs/_shared/rules/lang/python/deployment.md +523 -0
  21. package/configs/_shared/rules/lang/python/error-handling.md +330 -0
  22. package/configs/_shared/rules/lang/python/migrations.md +421 -0
  23. package/configs/_shared/rules/lang/python/python.md +172 -0
  24. package/configs/_shared/rules/lang/python/repository.md +383 -0
  25. package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
  26. package/configs/_shared/rules/lang/typescript/async.md +447 -0
  27. package/configs/_shared/rules/lang/typescript/generics.md +356 -0
  28. package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
  29. package/configs/_shared/rules/quality/error-handling.md +48 -0
  30. package/configs/_shared/rules/quality/logging.md +45 -0
  31. package/configs/_shared/rules/quality/observability.md +240 -0
  32. package/configs/_shared/rules/quality/testing-patterns.md +65 -0
  33. package/configs/_shared/rules/security/secrets-management.md +222 -0
  34. package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
  35. package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
  36. package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
  37. package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  38. package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  39. package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  40. package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
  41. package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
  42. package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
  43. package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
  44. package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
  45. package/configs/angular/CLAUDE.md +24 -216
  46. package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
  47. package/configs/angular/rules/core/resource.md +285 -0
  48. package/configs/angular/rules/core/signals.md +323 -0
  49. package/configs/angular/rules/http.md +338 -0
  50. package/configs/angular/rules/routing.md +291 -0
  51. package/configs/angular/rules/ssr.md +312 -0
  52. package/configs/angular/rules/state/signal-store.md +408 -0
  53. package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
  54. package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
  55. package/configs/angular/rules/ui/aria.md +422 -0
  56. package/configs/angular/rules/ui/forms.md +424 -0
  57. package/configs/angular/rules/ui/pipes-directives.md +335 -0
  58. package/configs/angular/{.claude/settings.json → settings.json} +3 -0
  59. package/configs/dotnet/CLAUDE.md +53 -286
  60. package/configs/dotnet/rules/background-services.md +552 -0
  61. package/configs/dotnet/rules/configuration.md +426 -0
  62. package/configs/dotnet/rules/ddd.md +447 -0
  63. package/configs/dotnet/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/rules/mediatr.md +320 -0
  65. package/configs/dotnet/rules/middleware.md +489 -0
  66. package/configs/dotnet/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/rules/validation.md +388 -0
  68. package/configs/dotnet/settings.json +29 -0
  69. package/configs/fastapi/CLAUDE.md +144 -0
  70. package/configs/fastapi/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/rules/dependencies.md +170 -0
  72. package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
  73. package/configs/fastapi/rules/lifespan.md +274 -0
  74. package/configs/fastapi/rules/middleware.md +229 -0
  75. package/configs/fastapi/rules/pydantic.md +433 -0
  76. package/configs/fastapi/rules/responses.md +251 -0
  77. package/configs/fastapi/rules/routers.md +202 -0
  78. package/configs/fastapi/rules/security.md +222 -0
  79. package/configs/fastapi/rules/testing.md +251 -0
  80. package/configs/fastapi/rules/websockets.md +298 -0
  81. package/configs/fastapi/settings.json +35 -0
  82. package/configs/flask/CLAUDE.md +166 -0
  83. package/configs/flask/rules/blueprints.md +208 -0
  84. package/configs/flask/rules/cli.md +285 -0
  85. package/configs/flask/rules/configuration.md +281 -0
  86. package/configs/flask/rules/context.md +238 -0
  87. package/configs/flask/rules/error-handlers.md +278 -0
  88. package/configs/flask/rules/extensions.md +278 -0
  89. package/configs/flask/rules/flask.md +171 -0
  90. package/configs/flask/rules/marshmallow.md +206 -0
  91. package/configs/flask/rules/security.md +267 -0
  92. package/configs/flask/rules/testing.md +284 -0
  93. package/configs/flask/settings.json +35 -0
  94. package/configs/nestjs/CLAUDE.md +57 -215
  95. package/configs/nestjs/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/rules/filters.md +376 -0
  97. package/configs/nestjs/rules/interceptors.md +317 -0
  98. package/configs/nestjs/rules/middleware.md +321 -0
  99. package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
  100. package/configs/nestjs/rules/pipes.md +351 -0
  101. package/configs/nestjs/rules/websockets.md +451 -0
  102. package/configs/nestjs/settings.json +31 -0
  103. package/configs/nextjs/CLAUDE.md +69 -331
  104. package/configs/nextjs/rules/api-routes.md +358 -0
  105. package/configs/nextjs/rules/authentication.md +355 -0
  106. package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
  107. package/configs/nextjs/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/rules/database.md +400 -0
  109. package/configs/nextjs/rules/middleware.md +303 -0
  110. package/configs/nextjs/rules/routing.md +324 -0
  111. package/configs/nextjs/rules/seo.md +350 -0
  112. package/configs/nextjs/rules/server-actions.md +353 -0
  113. package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
  114. package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
  115. package/package.json +24 -9
  116. package/src/cli.js +218 -0
  117. package/src/config.js +63 -0
  118. package/src/index.js +4 -0
  119. package/src/installer.js +414 -0
  120. package/src/merge.js +109 -0
  121. package/src/tech-config.json +45 -0
  122. package/src/utils.js +88 -0
  123. package/configs/dotnet/.claude/settings.json +0 -9
  124. package/configs/nestjs/.claude/settings.json +0 -15
  125. package/configs/python/.claude/rules/flask.md +0 -332
  126. package/configs/python/.claude/settings.json +0 -18
  127. package/configs/python/CLAUDE.md +0 -273
  128. package/src/install.js +0 -315
  129. /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
  130. /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
  131. /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
  132. /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
  133. /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
  134. /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
  135. /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
  136. /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
  137. /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
  138. /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
  139. /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
  140. /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
  141. /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
  142. /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
  143. /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
  144. /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
  145. /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
@@ -0,0 +1,433 @@
1
+ ---
2
+ paths:
3
+ - "**/schemas/**/*.py"
4
+ - "**/models/**/*.py"
5
+ - "**/*_schema.py"
6
+ - "**/*_model.py"
7
+ ---
8
+
9
+ # Python Validation (Pydantic)
10
+
11
+ ## Basic Models
12
+
13
+ ```python
14
+ # schemas/user.py
15
+ from datetime import datetime
16
+ from typing import Optional
17
+ from pydantic import BaseModel, EmailStr, Field, field_validator
18
+ from uuid import UUID
19
+
20
+
21
+ class UserBase(BaseModel):
22
+ email: EmailStr
23
+ name: str = Field(..., min_length=2, max_length=100)
24
+
25
+
26
+ class UserCreate(UserBase):
27
+ password: str = Field(..., min_length=8)
28
+
29
+ @field_validator("password")
30
+ @classmethod
31
+ def validate_password(cls, v: str) -> str:
32
+ if not any(c.isupper() for c in v):
33
+ raise ValueError("Password must contain uppercase letter")
34
+ if not any(c.islower() for c in v):
35
+ raise ValueError("Password must contain lowercase letter")
36
+ if not any(c.isdigit() for c in v):
37
+ raise ValueError("Password must contain digit")
38
+ if not any(c in "!@#$%^&*" for c in v):
39
+ raise ValueError("Password must contain special character")
40
+ return v
41
+
42
+
43
+ class UserUpdate(BaseModel):
44
+ name: Optional[str] = Field(None, min_length=2, max_length=100)
45
+ phone: Optional[str] = None
46
+
47
+
48
+ class UserResponse(UserBase):
49
+ id: UUID
50
+ is_active: bool
51
+ created_at: datetime
52
+
53
+ model_config = {"from_attributes": True}
54
+ ```
55
+
56
+ ## Nested Models
57
+
58
+ ```python
59
+ # schemas/order.py
60
+ from pydantic import BaseModel, Field, model_validator
61
+ from typing import List
62
+ from decimal import Decimal
63
+
64
+
65
+ class OrderItem(BaseModel):
66
+ product_id: UUID
67
+ quantity: int = Field(..., gt=0, le=100)
68
+ price: Decimal = Field(..., gt=0, decimal_places=2)
69
+
70
+
71
+ class OrderCreate(BaseModel):
72
+ items: List[OrderItem] = Field(..., min_length=1)
73
+ shipping_address: str = Field(..., min_length=10)
74
+ notes: Optional[str] = None
75
+
76
+ @model_validator(mode="after")
77
+ def validate_order(self) -> "OrderCreate":
78
+ total_items = sum(item.quantity for item in self.items)
79
+ if total_items > 100:
80
+ raise ValueError("Maximum 100 items per order")
81
+ return self
82
+
83
+
84
+ class OrderResponse(BaseModel):
85
+ id: UUID
86
+ items: List[OrderItem]
87
+ total: Decimal
88
+ status: str
89
+ created_at: datetime
90
+
91
+ model_config = {"from_attributes": True}
92
+ ```
93
+
94
+ ## Custom Types
95
+
96
+ ```python
97
+ # schemas/types.py
98
+ from typing import Annotated
99
+ from pydantic import AfterValidator, BeforeValidator
100
+ import re
101
+
102
+
103
+ def validate_phone(v: str) -> str:
104
+ pattern = r"^\+?[1-9]\d{1,14}$"
105
+ if not re.match(pattern, v):
106
+ raise ValueError("Invalid phone number format")
107
+ return v
108
+
109
+
110
+ def validate_slug(v: str) -> str:
111
+ pattern = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
112
+ if not re.match(pattern, v):
113
+ raise ValueError("Invalid slug format")
114
+ return v
115
+
116
+
117
+ def normalize_email(v: str) -> str:
118
+ return v.lower().strip()
119
+
120
+
121
+ PhoneNumber = Annotated[str, AfterValidator(validate_phone)]
122
+ Slug = Annotated[str, AfterValidator(validate_slug)]
123
+ NormalizedEmail = Annotated[str, BeforeValidator(normalize_email)]
124
+
125
+
126
+ # Usage
127
+ class UserCreate(BaseModel):
128
+ email: NormalizedEmail
129
+ phone: Optional[PhoneNumber] = None
130
+
131
+
132
+ class PostCreate(BaseModel):
133
+ title: str
134
+ slug: Slug
135
+ ```
136
+
137
+ ## Enum Validation
138
+
139
+ ```python
140
+ # schemas/enums.py
141
+ from enum import Enum
142
+
143
+
144
+ class UserRole(str, Enum):
145
+ USER = "user"
146
+ ADMIN = "admin"
147
+ MODERATOR = "moderator"
148
+
149
+
150
+ class OrderStatus(str, Enum):
151
+ PENDING = "pending"
152
+ PROCESSING = "processing"
153
+ SHIPPED = "shipped"
154
+ DELIVERED = "delivered"
155
+ CANCELLED = "cancelled"
156
+
157
+
158
+ # Usage
159
+ class UserCreate(BaseModel):
160
+ email: EmailStr
161
+ role: UserRole = UserRole.USER
162
+
163
+
164
+ class OrderUpdate(BaseModel):
165
+ status: OrderStatus
166
+ ```
167
+
168
+ ## Computed Fields
169
+
170
+ ```python
171
+ # schemas/product.py
172
+ from pydantic import BaseModel, computed_field
173
+ from decimal import Decimal
174
+
175
+
176
+ class Product(BaseModel):
177
+ name: str
178
+ price: Decimal
179
+ discount_percent: int = 0
180
+
181
+ @computed_field
182
+ @property
183
+ def discounted_price(self) -> Decimal:
184
+ discount = self.price * self.discount_percent / 100
185
+ return self.price - discount
186
+
187
+ @computed_field
188
+ @property
189
+ def display_name(self) -> str:
190
+ if self.discount_percent > 0:
191
+ return f"{self.name} ({self.discount_percent}% off)"
192
+ return self.name
193
+ ```
194
+
195
+ ## Conditional Validation
196
+
197
+ ```python
198
+ # schemas/payment.py
199
+ from pydantic import BaseModel, model_validator
200
+ from typing import Optional
201
+
202
+
203
+ class PaymentMethod(str, Enum):
204
+ CREDIT_CARD = "credit_card"
205
+ BANK_TRANSFER = "bank_transfer"
206
+ PAYPAL = "paypal"
207
+
208
+
209
+ class PaymentCreate(BaseModel):
210
+ method: PaymentMethod
211
+ amount: Decimal
212
+
213
+ # Credit card fields
214
+ card_number: Optional[str] = None
215
+ expiry_date: Optional[str] = None
216
+ cvv: Optional[str] = None
217
+
218
+ # Bank transfer fields
219
+ bank_account: Optional[str] = None
220
+ routing_number: Optional[str] = None
221
+
222
+ # PayPal fields
223
+ paypal_email: Optional[EmailStr] = None
224
+
225
+ @model_validator(mode="after")
226
+ def validate_payment_details(self) -> "PaymentCreate":
227
+ if self.method == PaymentMethod.CREDIT_CARD:
228
+ if not all([self.card_number, self.expiry_date, self.cvv]):
229
+ raise ValueError("Card details required for credit card payment")
230
+
231
+ elif self.method == PaymentMethod.BANK_TRANSFER:
232
+ if not all([self.bank_account, self.routing_number]):
233
+ raise ValueError("Bank details required for bank transfer")
234
+
235
+ elif self.method == PaymentMethod.PAYPAL:
236
+ if not self.paypal_email:
237
+ raise ValueError("PayPal email required")
238
+
239
+ return self
240
+ ```
241
+
242
+ ## Generic Pagination
243
+
244
+ ```python
245
+ # schemas/pagination.py
246
+ from typing import Generic, TypeVar, List
247
+ from pydantic import BaseModel
248
+
249
+ T = TypeVar("T")
250
+
251
+
252
+ class PaginatedResponse(BaseModel, Generic[T]):
253
+ items: List[T]
254
+ total: int
255
+ page: int
256
+ size: int
257
+ pages: int
258
+
259
+ @classmethod
260
+ def create(
261
+ cls,
262
+ items: List[T],
263
+ total: int,
264
+ page: int,
265
+ size: int,
266
+ ) -> "PaginatedResponse[T]":
267
+ return cls(
268
+ items=items,
269
+ total=total,
270
+ page=page,
271
+ size=size,
272
+ pages=(total + size - 1) // size,
273
+ )
274
+
275
+
276
+ class PaginationParams(BaseModel):
277
+ page: int = Field(1, ge=1)
278
+ size: int = Field(10, ge=1, le=100)
279
+
280
+ @property
281
+ def offset(self) -> int:
282
+ return (self.page - 1) * self.size
283
+
284
+
285
+ # Usage
286
+ # PaginatedResponse[UserResponse]
287
+ ```
288
+
289
+ ## FastAPI Integration
290
+
291
+ ```python
292
+ # api/users.py
293
+ from fastapi import APIRouter, HTTPException, Depends, Query
294
+ from pydantic import ValidationError
295
+
296
+ router = APIRouter(prefix="/users", tags=["users"])
297
+
298
+
299
+ @router.post("/", response_model=UserResponse, status_code=201)
300
+ async def create_user(
301
+ user: UserCreate,
302
+ db: AsyncSession = Depends(get_db),
303
+ ) -> UserResponse:
304
+ # Validation already done by Pydantic
305
+ existing = await db.execute(
306
+ select(User).where(User.email == user.email)
307
+ )
308
+ if existing.scalar_one_or_none():
309
+ raise HTTPException(status_code=409, detail="Email already registered")
310
+
311
+ new_user = User(**user.model_dump())
312
+ db.add(new_user)
313
+ await db.commit()
314
+ await db.refresh(new_user)
315
+
316
+ return UserResponse.model_validate(new_user)
317
+
318
+
319
+ @router.get("/", response_model=PaginatedResponse[UserResponse])
320
+ async def list_users(
321
+ pagination: PaginationParams = Depends(),
322
+ search: Optional[str] = Query(None, min_length=1),
323
+ db: AsyncSession = Depends(get_db),
324
+ ) -> PaginatedResponse[UserResponse]:
325
+ query = select(User)
326
+
327
+ if search:
328
+ query = query.where(User.name.ilike(f"%{search}%"))
329
+
330
+ total = await db.scalar(select(func.count()).select_from(query.subquery()))
331
+ users = await db.scalars(
332
+ query.offset(pagination.offset).limit(pagination.size)
333
+ )
334
+
335
+ return PaginatedResponse.create(
336
+ items=[UserResponse.model_validate(u) for u in users],
337
+ total=total,
338
+ page=pagination.page,
339
+ size=pagination.size,
340
+ )
341
+ ```
342
+
343
+ ## Custom Error Messages
344
+
345
+ ```python
346
+ # schemas/user.py
347
+ from pydantic import BaseModel, Field
348
+
349
+
350
+ class UserCreate(BaseModel):
351
+ email: EmailStr = Field(
352
+ ...,
353
+ description="User email address",
354
+ json_schema_extra={"example": "user@example.com"},
355
+ )
356
+ password: str = Field(
357
+ ...,
358
+ min_length=8,
359
+ max_length=100,
360
+ description="User password (min 8 characters)",
361
+ )
362
+
363
+ model_config = {
364
+ "json_schema_extra": {
365
+ "examples": [
366
+ {
367
+ "email": "john@example.com",
368
+ "password": "SecurePass123!",
369
+ }
370
+ ]
371
+ }
372
+ }
373
+ ```
374
+
375
+ ## Anti-patterns
376
+
377
+ ```python
378
+ # BAD: Using dict instead of Pydantic model
379
+ @router.post("/users")
380
+ async def create_user(data: dict): # No validation!
381
+ email = data.get("email")
382
+
383
+
384
+ # GOOD: Use Pydantic models
385
+ @router.post("/users")
386
+ async def create_user(data: UserCreate):
387
+ email = data.email # Validated and typed
388
+
389
+
390
+ # BAD: Manual validation
391
+ class UserCreate(BaseModel):
392
+ email: str
393
+
394
+ def validate_email(self):
395
+ if "@" not in self.email:
396
+ raise ValueError("Invalid email")
397
+
398
+
399
+ # GOOD: Use built-in validators
400
+ class UserCreate(BaseModel):
401
+ email: EmailStr # Automatic validation
402
+
403
+
404
+ # BAD: Catching all exceptions
405
+ try:
406
+ user = UserCreate(**data)
407
+ except Exception as e:
408
+ return {"error": str(e)}
409
+
410
+
411
+ # GOOD: Catch specific validation errors
412
+ from pydantic import ValidationError
413
+
414
+ try:
415
+ user = UserCreate(**data)
416
+ except ValidationError as e:
417
+ return {"errors": e.errors()}
418
+
419
+
420
+ # BAD: Not using model_config for ORM
421
+ class UserResponse(BaseModel):
422
+ id: UUID
423
+ name: str
424
+ # This won't work with SQLAlchemy objects
425
+
426
+
427
+ # GOOD: Enable from_attributes
428
+ class UserResponse(BaseModel):
429
+ id: UUID
430
+ name: str
431
+
432
+ model_config = {"from_attributes": True}
433
+ ```
@@ -0,0 +1,251 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # FastAPI Response Patterns
7
+
8
+ ## Response Models
9
+
10
+ ```python
11
+ from pydantic import BaseModel, ConfigDict
12
+
13
+ # GOOD - explicit response model
14
+ class UserResponse(BaseModel):
15
+ id: int
16
+ email: str
17
+ name: str
18
+ created_at: datetime
19
+
20
+ model_config = ConfigDict(from_attributes=True)
21
+
22
+ @router.get("/users/{user_id}", response_model=UserResponse)
23
+ async def get_user(user_id: int, db: DbSession) -> User:
24
+ user = await db.get(User, user_id)
25
+ if not user:
26
+ raise HTTPException(404, "User not found")
27
+ return user # Automatically serialized to UserResponse
28
+ ```
29
+
30
+ ## Pagination Response
31
+
32
+ ```python
33
+ from typing import Generic, TypeVar
34
+
35
+ T = TypeVar("T")
36
+
37
+ class Page(BaseModel, Generic[T]):
38
+ items: list[T]
39
+ total: int
40
+ page: int
41
+ size: int
42
+ pages: int
43
+
44
+ @classmethod
45
+ def create(cls, items: list[T], total: int, page: int, size: int) -> "Page[T]":
46
+ return cls(
47
+ items=items,
48
+ total=total,
49
+ page=page,
50
+ size=size,
51
+ pages=(total + size - 1) // size,
52
+ )
53
+
54
+ @router.get("/users", response_model=Page[UserResponse])
55
+ async def list_users(
56
+ page: int = Query(1, ge=1),
57
+ size: int = Query(20, ge=1, le=100),
58
+ db: DbSession,
59
+ ) -> Page[User]:
60
+ total = await db.scalar(select(func.count()).select_from(User))
61
+
62
+ users = await db.scalars(
63
+ select(User)
64
+ .offset((page - 1) * size)
65
+ .limit(size)
66
+ )
67
+
68
+ return Page.create(list(users), total, page, size)
69
+ ```
70
+
71
+ ## Response Classes
72
+
73
+ ```python
74
+ from fastapi.responses import (
75
+ JSONResponse,
76
+ HTMLResponse,
77
+ PlainTextResponse,
78
+ RedirectResponse,
79
+ StreamingResponse,
80
+ FileResponse,
81
+ )
82
+
83
+ # JSON with custom status and headers
84
+ @router.post("/users")
85
+ async def create_user(data: UserCreate) -> JSONResponse:
86
+ user = await user_service.create(data)
87
+ return JSONResponse(
88
+ content={"id": user.id, "message": "User created"},
89
+ status_code=201,
90
+ headers={"X-Custom-Header": "value"},
91
+ )
92
+
93
+ # HTML response
94
+ @router.get("/page", response_class=HTMLResponse)
95
+ async def get_page() -> str:
96
+ return "<html><body><h1>Hello</h1></body></html>"
97
+
98
+ # Redirect
99
+ @router.get("/old-path")
100
+ async def redirect() -> RedirectResponse:
101
+ return RedirectResponse(url="/new-path", status_code=301)
102
+
103
+ # File download
104
+ @router.get("/download/{filename}")
105
+ async def download_file(filename: str) -> FileResponse:
106
+ return FileResponse(
107
+ path=f"files/{filename}",
108
+ filename=filename,
109
+ media_type="application/octet-stream",
110
+ )
111
+ ```
112
+
113
+ ## Streaming Response
114
+
115
+ ```python
116
+ import asyncio
117
+ from typing import AsyncGenerator
118
+
119
+ async def generate_data() -> AsyncGenerator[bytes, None]:
120
+ for i in range(100):
121
+ yield f"data: {i}\n\n".encode()
122
+ await asyncio.sleep(0.1)
123
+
124
+ @router.get("/stream")
125
+ async def stream_data() -> StreamingResponse:
126
+ return StreamingResponse(
127
+ generate_data(),
128
+ media_type="text/event-stream",
129
+ )
130
+
131
+ # Stream large file
132
+ @router.get("/large-file")
133
+ async def stream_file() -> StreamingResponse:
134
+ async def iterfile():
135
+ async with aiofiles.open("large_file.csv", "rb") as f:
136
+ while chunk := await f.read(8192):
137
+ yield chunk
138
+
139
+ return StreamingResponse(
140
+ iterfile(),
141
+ media_type="text/csv",
142
+ headers={"Content-Disposition": "attachment; filename=data.csv"},
143
+ )
144
+ ```
145
+
146
+ ## Error Responses
147
+
148
+ ```python
149
+ from fastapi import HTTPException
150
+ from fastapi.responses import JSONResponse
151
+
152
+ # Standard HTTP exception
153
+ @router.get("/users/{user_id}")
154
+ async def get_user(user_id: int) -> UserResponse:
155
+ user = await db.get(User, user_id)
156
+ if not user:
157
+ raise HTTPException(
158
+ status_code=404,
159
+ detail="User not found",
160
+ headers={"X-Error-Code": "USER_NOT_FOUND"},
161
+ )
162
+ return user
163
+
164
+ # Custom error response model
165
+ class ErrorResponse(BaseModel):
166
+ error: str
167
+ detail: str
168
+ request_id: str | None = None
169
+
170
+ @router.get(
171
+ "/users/{user_id}",
172
+ responses={
173
+ 200: {"model": UserResponse},
174
+ 404: {"model": ErrorResponse, "description": "User not found"},
175
+ 500: {"model": ErrorResponse, "description": "Internal server error"},
176
+ },
177
+ )
178
+ async def get_user(user_id: int) -> UserResponse:
179
+ ...
180
+
181
+ # Global exception handler
182
+ @app.exception_handler(ValueError)
183
+ async def value_error_handler(request: Request, exc: ValueError) -> JSONResponse:
184
+ return JSONResponse(
185
+ status_code=400,
186
+ content={"error": "Bad Request", "detail": str(exc)},
187
+ )
188
+ ```
189
+
190
+ ## Response Headers
191
+
192
+ ```python
193
+ from fastapi import Response
194
+
195
+ @router.get("/data")
196
+ async def get_data(response: Response) -> dict:
197
+ response.headers["X-Custom-Header"] = "custom-value"
198
+ response.headers["Cache-Control"] = "max-age=3600"
199
+ return {"data": "value"}
200
+
201
+ # Set cookies
202
+ @router.post("/login")
203
+ async def login(response: Response) -> dict:
204
+ response.set_cookie(
205
+ key="session_id",
206
+ value="abc123",
207
+ httponly=True,
208
+ secure=True,
209
+ samesite="lax",
210
+ max_age=3600,
211
+ )
212
+ return {"status": "logged_in"}
213
+
214
+ # Delete cookie
215
+ @router.post("/logout")
216
+ async def logout(response: Response) -> dict:
217
+ response.delete_cookie("session_id")
218
+ return {"status": "logged_out"}
219
+ ```
220
+
221
+ ## Background Response
222
+
223
+ ```python
224
+ from fastapi import BackgroundTasks
225
+
226
+ @router.post("/send-email")
227
+ async def send_email(
228
+ email: EmailSchema,
229
+ background_tasks: BackgroundTasks,
230
+ ) -> dict:
231
+ # Return immediately
232
+ background_tasks.add_task(send_email_async, email.to, email.subject, email.body)
233
+ return {"message": "Email queued"}
234
+ ```
235
+
236
+ ## No Content Response
237
+
238
+ ```python
239
+ from fastapi import Response, status
240
+
241
+ @router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
242
+ async def delete_user(user_id: int, db: DbSession) -> Response:
243
+ user = await db.get(User, user_id)
244
+ if not user:
245
+ raise HTTPException(404, "User not found")
246
+
247
+ await db.delete(user)
248
+ await db.commit()
249
+
250
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
251
+ ```