claude-code-pilot 3.2.0 → 3.3.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +14 -9
  3. package/bin/install.js +124 -16
  4. package/manifest.json +18 -3
  5. package/package.json +3 -2
  6. package/src/agents/django-build-resolver.md +252 -0
  7. package/src/agents/django-reviewer.md +169 -0
  8. package/src/agents/fastapi-reviewer.md +79 -0
  9. package/src/agents/fsharp-reviewer.md +109 -0
  10. package/src/agents/swift-build-resolver.md +170 -0
  11. package/src/agents/swift-reviewer.md +116 -0
  12. package/src/commands/ccp/cost-report.md +107 -0
  13. package/src/commands/ccp/intel.md +3 -3
  14. package/src/commands/ccp/mvp-phase.md +45 -0
  15. package/src/commands/ccp/plan-prd.md +160 -0
  16. package/src/commands/ccp/pr-ecc.md +184 -0
  17. package/src/commands/ccp/security-scan.md +74 -0
  18. package/src/hooks/ccp-bash-hook-dispatcher.js +96 -0
  19. package/src/hooks/ccp-context-monitor.js +23 -0
  20. package/src/hooks/ccp-doc-file-warning.js +93 -0
  21. package/src/hooks/ccp-pre-bash-dispatcher.js +24 -0
  22. package/src/hooks/ccp-write-gateguard.js +868 -0
  23. package/src/lib/project-detect.js +0 -2
  24. package/src/lib/shell-substitution.js +499 -0
  25. package/src/pilot/references/execute-mvp-tdd.md +81 -0
  26. package/src/pilot/references/mvp-concepts.md +49 -0
  27. package/src/pilot/references/planner-graphify-auto-update.md +67 -0
  28. package/src/pilot/references/planner-human-verify-mode.md +57 -0
  29. package/src/pilot/references/planner-mvp-mode.md +53 -0
  30. package/src/pilot/references/skeleton-template.md +48 -0
  31. package/src/pilot/references/spidr-splitting.md +69 -0
  32. package/src/pilot/references/user-story-template.md +58 -0
  33. package/src/pilot/references/verify-mvp-mode.md +85 -0
  34. package/src/pilot/references/worktree-path-safety.md +89 -0
  35. package/src/pilot/workflows/help.md +5 -0
  36. package/src/pilot/workflows/mvp-phase.md +199 -0
  37. package/src/skills/agent-architecture-audit/SKILL.md +256 -0
  38. package/src/skills/agent-harness-design/SKILL.md +73 -0
  39. package/src/skills/angular-developer/SKILL.md +154 -0
  40. package/src/skills/angular-developer/references/angular-animations.md +160 -0
  41. package/src/skills/angular-developer/references/angular-aria.md +410 -0
  42. package/src/skills/angular-developer/references/cli.md +86 -0
  43. package/src/skills/angular-developer/references/component-harnesses.md +59 -0
  44. package/src/skills/angular-developer/references/component-styling.md +91 -0
  45. package/src/skills/angular-developer/references/components.md +117 -0
  46. package/src/skills/angular-developer/references/creating-services.md +97 -0
  47. package/src/skills/angular-developer/references/data-resolvers.md +69 -0
  48. package/src/skills/angular-developer/references/define-routes.md +67 -0
  49. package/src/skills/angular-developer/references/defining-providers.md +72 -0
  50. package/src/skills/angular-developer/references/di-fundamentals.md +120 -0
  51. package/src/skills/angular-developer/references/e2e-testing.md +56 -0
  52. package/src/skills/angular-developer/references/effects.md +83 -0
  53. package/src/skills/angular-developer/references/hierarchical-injectors.md +43 -0
  54. package/src/skills/angular-developer/references/host-elements.md +80 -0
  55. package/src/skills/angular-developer/references/injection-context.md +63 -0
  56. package/src/skills/angular-developer/references/inputs.md +101 -0
  57. package/src/skills/angular-developer/references/linked-signal.md +59 -0
  58. package/src/skills/angular-developer/references/loading-strategies.md +61 -0
  59. package/src/skills/angular-developer/references/mcp.md +108 -0
  60. package/src/skills/angular-developer/references/navigate-to-routes.md +69 -0
  61. package/src/skills/angular-developer/references/outputs.md +86 -0
  62. package/src/skills/angular-developer/references/reactive-forms.md +122 -0
  63. package/src/skills/angular-developer/references/rendering-strategies.md +44 -0
  64. package/src/skills/angular-developer/references/resource.md +77 -0
  65. package/src/skills/angular-developer/references/route-animations.md +56 -0
  66. package/src/skills/angular-developer/references/route-guards.md +52 -0
  67. package/src/skills/angular-developer/references/router-lifecycle.md +45 -0
  68. package/src/skills/angular-developer/references/router-testing.md +87 -0
  69. package/src/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
  70. package/src/skills/angular-developer/references/signal-forms.md +795 -0
  71. package/src/skills/angular-developer/references/signals-overview.md +94 -0
  72. package/src/skills/angular-developer/references/tailwind-css.md +69 -0
  73. package/src/skills/angular-developer/references/template-driven-forms.md +114 -0
  74. package/src/skills/angular-developer/references/testing-fundamentals.md +65 -0
  75. package/src/skills/error-handling/SKILL.md +376 -0
  76. package/src/skills/fastapi-patterns/SKILL.md +327 -0
  77. package/src/skills/flox-environments/SKILL.md +496 -0
  78. package/src/skills/fsharp-testing/SKILL.md +280 -0
  79. package/src/skills/ios-icon-gen/SKILL.md +157 -0
  80. package/src/skills/ios-icon-gen/scripts/generate_icons.swift +258 -0
  81. package/src/skills/ios-icon-gen/scripts/iconify_gen.sh +235 -0
  82. package/src/skills/make-interfaces-feel-better/SKILL.md +151 -0
  83. package/src/skills/mysql-patterns/SKILL.md +412 -0
  84. package/src/skills/plan-orchestrate/SKILL.md +220 -0
  85. package/src/skills/prisma-patterns/SKILL.md +371 -0
  86. package/src/skills/production-audit/SKILL.md +206 -0
  87. package/src/skills/security-scan/references/agentshield-policy-exception/candidate-playbook.md +49 -0
  88. package/src/skills/security-scan/references/agentshield-policy-exception/report.json +35 -0
  89. package/src/skills/security-scan/references/agentshield-policy-exception/scenario.json +62 -0
  90. package/src/skills/security-scan/references/agentshield-policy-exception/trace.json +45 -0
  91. package/src/skills/security-scan/references/agentshield-policy-exception/verifier-result.json +35 -0
  92. package/src/skills/vite-patterns/SKILL.md +449 -0
  93. package/src/skills/windows-desktop-e2e/SKILL.md +887 -0
@@ -0,0 +1,376 @@
1
+ ---
2
+ name: error-handling
3
+ description: Patterns for robust error handling across TypeScript, Python, and Go. Covers typed errors, error boundaries, retries, circuit breakers, and user-facing error messages.
4
+ origin: ECC
5
+ ---
6
+
7
+ # Error Handling Patterns
8
+
9
+ Consistent, robust error handling patterns for production applications.
10
+
11
+ ## When to Activate
12
+
13
+ - Designing error types or exception hierarchies for a new module or service
14
+ - Adding retry logic or circuit breakers for unreliable external dependencies
15
+ - Reviewing API endpoints for missing error handling
16
+ - Implementing user-facing error messages and feedback
17
+ - Debugging cascading failures or silent error swallowing
18
+
19
+ ## Core Principles
20
+
21
+ 1. **Fail fast and loudly** — surface errors at the boundary where they occur; don't bury them
22
+ 2. **Typed errors over string messages** — errors are first-class values with structure
23
+ 3. **User messages ≠ developer messages** — show friendly text to users, log full context server-side
24
+ 4. **Never swallow errors silently** — every `catch` block must either handle, re-throw, or log
25
+ 5. **Errors are part of your API contract** — document every error code a client may receive
26
+
27
+ ## TypeScript / JavaScript
28
+
29
+ ### Typed Error Classes
30
+
31
+ ```typescript
32
+ // Define an error hierarchy for your domain
33
+ export class AppError extends Error {
34
+ constructor(
35
+ message: string,
36
+ public readonly code: string,
37
+ public readonly statusCode: number = 500,
38
+ public readonly details?: unknown,
39
+ ) {
40
+ super(message)
41
+ this.name = this.constructor.name
42
+ // Maintain correct prototype chain in transpiled ES5 JavaScript.
43
+ // Required for `instanceof` checks (e.g., `error instanceof NotFoundError`)
44
+ // to work correctly when extending the built-in Error class.
45
+ Object.setPrototypeOf(this, new.target.prototype)
46
+ }
47
+ }
48
+
49
+ export class NotFoundError extends AppError {
50
+ constructor(resource: string, id: string) {
51
+ super(`${resource} not found: ${id}`, 'NOT_FOUND', 404)
52
+ }
53
+ }
54
+
55
+ export class ValidationError extends AppError {
56
+ constructor(message: string, details: { field: string; message: string }[]) {
57
+ super(message, 'VALIDATION_ERROR', 422, details)
58
+ }
59
+ }
60
+
61
+ export class UnauthorizedError extends AppError {
62
+ constructor(reason = 'Authentication required') {
63
+ super(reason, 'UNAUTHORIZED', 401)
64
+ }
65
+ }
66
+
67
+ export class RateLimitError extends AppError {
68
+ constructor(public readonly retryAfterMs: number) {
69
+ super('Rate limit exceeded', 'RATE_LIMITED', 429)
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### Result Pattern (no-throw style)
75
+
76
+ For operations where failure is expected and common (parsing, external calls):
77
+
78
+ ```typescript
79
+ type Result<T, E = AppError> =
80
+ | { ok: true; value: T }
81
+ | { ok: false; error: E }
82
+
83
+ function ok<T>(value: T): Result<T> {
84
+ return { ok: true, value }
85
+ }
86
+
87
+ function err<E>(error: E): Result<never, E> {
88
+ return { ok: false, error }
89
+ }
90
+
91
+ // Usage
92
+ async function fetchUser(id: string): Promise<Result<User>> {
93
+ try {
94
+ const user = await db.users.findUnique({ where: { id } })
95
+ if (!user) return err(new NotFoundError('User', id))
96
+ return ok(user)
97
+ } catch (e) {
98
+ return err(new AppError('Database error', 'DB_ERROR'))
99
+ }
100
+ }
101
+
102
+ const result = await fetchUser('abc-123')
103
+ if (!result.ok) {
104
+ // TypeScript knows result.error here
105
+ logger.error('Failed to fetch user', { error: result.error })
106
+ return
107
+ }
108
+ // TypeScript knows result.value here
109
+ console.log(result.value.email)
110
+ ```
111
+
112
+ ### API Error Handler (Next.js / Express)
113
+
114
+ ```typescript
115
+ import { NextRequest, NextResponse } from 'next/server'
116
+
117
+ function handleApiError(error: unknown): NextResponse {
118
+ // Known application error
119
+ if (error instanceof AppError) {
120
+ return NextResponse.json(
121
+ {
122
+ error: {
123
+ code: error.code,
124
+ message: error.message,
125
+ ...(error.details ? { details: error.details } : {}),
126
+ },
127
+ },
128
+ { status: error.statusCode },
129
+ )
130
+ }
131
+
132
+ // Zod validation error
133
+ if (error instanceof z.ZodError) {
134
+ return NextResponse.json(
135
+ {
136
+ error: {
137
+ code: 'VALIDATION_ERROR',
138
+ message: 'Request validation failed',
139
+ details: error.issues.map(i => ({
140
+ field: i.path.join('.'),
141
+ message: i.message,
142
+ })),
143
+ },
144
+ },
145
+ { status: 422 },
146
+ )
147
+ }
148
+
149
+ // Unexpected error — log details, return generic message
150
+ console.error('Unexpected error:', error)
151
+ return NextResponse.json(
152
+ { error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } },
153
+ { status: 500 },
154
+ )
155
+ }
156
+
157
+ export async function POST(req: NextRequest) {
158
+ try {
159
+ // ... handler logic
160
+ } catch (error) {
161
+ return handleApiError(error)
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### React Error Boundary
167
+
168
+ ```typescript
169
+ import { Component, ErrorInfo, ReactNode } from 'react'
170
+
171
+ interface Props {
172
+ fallback: ReactNode
173
+ onError?: (error: Error, info: ErrorInfo) => void
174
+ children: ReactNode
175
+ }
176
+
177
+ interface State {
178
+ hasError: boolean
179
+ error: Error | null
180
+ }
181
+
182
+ export class ErrorBoundary extends Component<Props, State> {
183
+ state: State = { hasError: false, error: null }
184
+
185
+ static getDerivedStateFromError(error: Error): State {
186
+ return { hasError: true, error }
187
+ }
188
+
189
+ componentDidCatch(error: Error, info: ErrorInfo) {
190
+ this.props.onError?.(error, info)
191
+ console.error('Unhandled React error:', error, info)
192
+ }
193
+
194
+ render() {
195
+ if (this.state.hasError) return this.props.fallback
196
+ return this.props.children
197
+ }
198
+ }
199
+
200
+ // Usage
201
+ <ErrorBoundary fallback={<p>Something went wrong. Please refresh.</p>}>
202
+ <MyComponent />
203
+ </ErrorBoundary>
204
+ ```
205
+
206
+ ## Python
207
+
208
+ ### Custom Exception Hierarchy
209
+
210
+ ```python
211
+ class AppError(Exception):
212
+ """Base application error."""
213
+ def __init__(self, message: str, code: str, status_code: int = 500):
214
+ super().__init__(message)
215
+ self.code = code
216
+ self.status_code = status_code
217
+
218
+ class NotFoundError(AppError):
219
+ def __init__(self, resource: str, id: str):
220
+ super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)
221
+
222
+ class ValidationError(AppError):
223
+ def __init__(self, message: str, details: list[dict] | None = None):
224
+ super().__init__(message, "VALIDATION_ERROR", 422)
225
+ self.details = details or []
226
+ ```
227
+
228
+ ### FastAPI Global Exception Handler
229
+
230
+ ```python
231
+ from fastapi import FastAPI, Request
232
+ from fastapi.responses import JSONResponse
233
+
234
+ app = FastAPI()
235
+
236
+ @app.exception_handler(AppError)
237
+ async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
238
+ return JSONResponse(
239
+ status_code=exc.status_code,
240
+ content={"error": {"code": exc.code, "message": str(exc)}},
241
+ )
242
+
243
+ @app.exception_handler(Exception)
244
+ async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
245
+ # Log full details, return generic message
246
+ logger.exception("Unexpected error", exc_info=exc)
247
+ return JSONResponse(
248
+ status_code=500,
249
+ content={"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}},
250
+ )
251
+ ```
252
+
253
+ ## Go
254
+
255
+ ### Sentinel Errors and Error Wrapping
256
+
257
+ ```go
258
+ package domain
259
+
260
+ import "errors"
261
+
262
+ // Sentinel errors for type-checking
263
+ var (
264
+ ErrNotFound = errors.New("not found")
265
+ ErrUnauthorized = errors.New("unauthorized")
266
+ ErrConflict = errors.New("conflict")
267
+ )
268
+
269
+ // Wrap errors with context — never lose the original
270
+ func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
271
+ user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
272
+ if errors.Is(err, sql.ErrNoRows) {
273
+ return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
274
+ }
275
+ if err != nil {
276
+ return nil, fmt.Errorf("querying user %s: %w", id, err)
277
+ }
278
+ return user, nil
279
+ }
280
+
281
+ // At the handler level, unwrap to determine response
282
+ func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
283
+ user, err := h.service.GetUser(r.Context(), chi.URLParam(r, "id"))
284
+ if err != nil {
285
+ switch {
286
+ case errors.Is(err, domain.ErrNotFound):
287
+ writeError(w, http.StatusNotFound, "not_found", err.Error())
288
+ case errors.Is(err, domain.ErrUnauthorized):
289
+ writeError(w, http.StatusForbidden, "forbidden", "Access denied")
290
+ default:
291
+ slog.Error("unexpected error", "err", err)
292
+ writeError(w, http.StatusInternalServerError, "internal_error", "An unexpected error occurred")
293
+ }
294
+ return
295
+ }
296
+ writeJSON(w, http.StatusOK, user)
297
+ }
298
+ ```
299
+
300
+ ## Retry with Exponential Backoff
301
+
302
+ ```typescript
303
+ interface RetryOptions {
304
+ maxAttempts?: number
305
+ baseDelayMs?: number
306
+ maxDelayMs?: number
307
+ retryIf?: (error: unknown) => boolean
308
+ }
309
+
310
+ async function withRetry<T>(
311
+ fn: () => Promise<T>,
312
+ options: RetryOptions = {},
313
+ ): Promise<T> {
314
+ const {
315
+ maxAttempts = 3,
316
+ baseDelayMs = 500,
317
+ maxDelayMs = 10_000,
318
+ retryIf = () => true,
319
+ } = options
320
+
321
+ let lastError: unknown
322
+
323
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
324
+ try {
325
+ return await fn()
326
+ } catch (error) {
327
+ lastError = error
328
+ if (attempt === maxAttempts || !retryIf(error)) throw error
329
+
330
+ const jitter = Math.random() * baseDelayMs
331
+ const delay = Math.min(baseDelayMs * 2 ** (attempt - 1) + jitter, maxDelayMs)
332
+ await new Promise(resolve => setTimeout(resolve, delay))
333
+ }
334
+ }
335
+
336
+ throw lastError
337
+ }
338
+
339
+ // Usage: retry transient network errors, not 4xx
340
+ const data = await withRetry(() => fetch('/api/data').then(r => r.json()), {
341
+ maxAttempts: 3,
342
+ retryIf: (error) => !(error instanceof AppError && error.statusCode < 500),
343
+ })
344
+ ```
345
+
346
+ ## User-Facing Error Messages
347
+
348
+ Map error codes to human-readable messages. Keep technical details out of user-visible text.
349
+
350
+ ```typescript
351
+ const USER_ERROR_MESSAGES: Record<string, string> = {
352
+ NOT_FOUND: 'The requested item could not be found.',
353
+ UNAUTHORIZED: 'Please sign in to continue.',
354
+ FORBIDDEN: "You don't have permission to do that.",
355
+ VALIDATION_ERROR: 'Please check your input and try again.',
356
+ RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
357
+ INTERNAL_ERROR: 'Something went wrong on our end. Please try again later.',
358
+ }
359
+
360
+ export function getUserMessage(code: string): string {
361
+ return USER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR
362
+ }
363
+ ```
364
+
365
+ ## Error Handling Checklist
366
+
367
+ Before merging any code that touches error handling:
368
+
369
+ - [ ] Every `catch` block handles, re-throws, or logs — no silent swallowing
370
+ - [ ] API errors follow the standard envelope `{ error: { code, message } }`
371
+ - [ ] User-facing messages contain no stack traces or internal details
372
+ - [ ] Full error context is logged server-side
373
+ - [ ] Custom error classes extend a base `AppError` with a `code` field
374
+ - [ ] Async functions surface errors to callers — no fire-and-forget without fallback
375
+ - [ ] Retry logic only retries retriable errors (not 4xx client errors)
376
+ - [ ] React components are wrapped in `ErrorBoundary` for rendering errors
@@ -0,0 +1,327 @@
1
+ ---
2
+ name: fastapi-patterns
3
+ description: FastAPI patterns for async APIs, dependency injection, Pydantic request and response models, OpenAPI docs, tests, security, and production readiness.
4
+ origin: community
5
+ ---
6
+
7
+ # FastAPI Patterns
8
+
9
+ Production-oriented patterns for FastAPI services.
10
+
11
+ ## When to Use
12
+
13
+ - Building or reviewing a FastAPI app.
14
+ - Splitting routers, schemas, dependencies, and database access.
15
+ - Writing async endpoints that call a database or external service.
16
+ - Adding authentication, authorization, OpenAPI docs, tests, or deployment settings.
17
+ - Checking a FastAPI PR for copy-pasteable examples and production risks.
18
+
19
+ ## How It Works
20
+
21
+ Treat the FastAPI app as a thin HTTP layer over explicit dependencies and service code:
22
+
23
+ - `main.py` owns app construction, middleware, exception handlers, and router registration.
24
+ - `schemas/` owns Pydantic request and response models.
25
+ - `dependencies.py` owns database, auth, pagination, and request-scoped dependencies.
26
+ - `services/` or `crud/` owns business and persistence operations.
27
+ - `tests/` overrides dependencies instead of opening production resources.
28
+
29
+ Prefer small routers and explicit `response_model` declarations. Keep raw ORM objects, secrets, and framework globals out of response schemas.
30
+
31
+ ## Project Layout
32
+
33
+ ```text
34
+ app/
35
+ |-- main.py
36
+ |-- config.py
37
+ |-- dependencies.py
38
+ |-- exceptions.py
39
+ |-- api/
40
+ | `-- routes/
41
+ | |-- users.py
42
+ | `-- health.py
43
+ |-- core/
44
+ | |-- security.py
45
+ | `-- middleware.py
46
+ |-- db/
47
+ | |-- session.py
48
+ | `-- crud.py
49
+ |-- models/
50
+ |-- schemas/
51
+ `-- tests/
52
+ ```
53
+
54
+ ## Application Factory
55
+
56
+ Use a factory so tests and workers can build the app with controlled settings.
57
+
58
+ ```python
59
+ from contextlib import asynccontextmanager
60
+
61
+ from fastapi import FastAPI
62
+ from fastapi.middleware.cors import CORSMiddleware
63
+
64
+ from app.api.routes import health, users
65
+ from app.config import settings
66
+ from app.db.session import close_db, init_db
67
+ from app.exceptions import register_exception_handlers
68
+
69
+
70
+ @asynccontextmanager
71
+ async def lifespan(app: FastAPI):
72
+ await init_db()
73
+ yield
74
+ await close_db()
75
+
76
+
77
+ def create_app() -> FastAPI:
78
+ app = FastAPI(
79
+ title=settings.api_title,
80
+ version=settings.api_version,
81
+ lifespan=lifespan,
82
+ )
83
+
84
+ app.add_middleware(
85
+ CORSMiddleware,
86
+ allow_origins=settings.cors_origins,
87
+ allow_credentials=bool(settings.cors_origins),
88
+ allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
89
+ allow_headers=["Authorization", "Content-Type"],
90
+ )
91
+
92
+ register_exception_handlers(app)
93
+ app.include_router(health.router, prefix="/health", tags=["health"])
94
+ app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
95
+ return app
96
+
97
+
98
+ app = create_app()
99
+ ```
100
+
101
+ Do not use `allow_origins=["*"]` with `allow_credentials=True`; browsers reject that combination and Starlette disallows it for credentialed requests.
102
+
103
+ ## Pydantic Schemas
104
+
105
+ Keep request, update, and response models separate.
106
+
107
+ ```python
108
+ from datetime import datetime
109
+ from typing import Annotated
110
+ from uuid import UUID
111
+
112
+ from pydantic import BaseModel, ConfigDict, EmailStr, Field
113
+
114
+
115
+ class UserBase(BaseModel):
116
+ email: EmailStr
117
+ full_name: Annotated[str, Field(min_length=1, max_length=100)]
118
+
119
+
120
+ class UserCreate(UserBase):
121
+ password: Annotated[str, Field(min_length=12, max_length=128)]
122
+
123
+
124
+ class UserUpdate(BaseModel):
125
+ email: EmailStr | None = None
126
+ full_name: Annotated[str | None, Field(min_length=1, max_length=100)] = None
127
+
128
+
129
+ class UserResponse(UserBase):
130
+ model_config = ConfigDict(from_attributes=True)
131
+
132
+ id: UUID
133
+ created_at: datetime
134
+ updated_at: datetime
135
+ ```
136
+
137
+ Response models must never include password hashes, access tokens, refresh tokens, or internal authorization state.
138
+
139
+ ## Dependencies
140
+
141
+ Use dependency injection for request-scoped resources.
142
+
143
+ ```python
144
+ from collections.abc import AsyncIterator
145
+ from uuid import UUID
146
+
147
+ from fastapi import Depends, HTTPException, status
148
+ from fastapi.security import OAuth2PasswordBearer
149
+ from sqlalchemy.ext.asyncio import AsyncSession
150
+
151
+ from app.core.security import decode_token
152
+ from app.db.session import session_factory
153
+ from app.models.user import User
154
+
155
+
156
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
157
+
158
+
159
+ async def get_db() -> AsyncIterator[AsyncSession]:
160
+ async with session_factory() as session:
161
+ try:
162
+ yield session
163
+ await session.commit()
164
+ except Exception:
165
+ await session.rollback()
166
+ raise
167
+
168
+
169
+ async def get_current_user(
170
+ token: str = Depends(oauth2_scheme),
171
+ db: AsyncSession = Depends(get_db),
172
+ ) -> User:
173
+ payload = decode_token(token)
174
+ user_id = UUID(payload["sub"])
175
+ user = await db.get(User, user_id)
176
+ if user is None:
177
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
178
+ return user
179
+ ```
180
+
181
+ Avoid creating sessions, clients, or credentials inline inside route handlers.
182
+
183
+ ## Async Endpoints
184
+
185
+ Keep route handlers async when they perform I/O, and use async libraries inside them.
186
+
187
+ ```python
188
+ from fastapi import APIRouter, Depends, Query
189
+ from sqlalchemy import select
190
+ from sqlalchemy.ext.asyncio import AsyncSession
191
+
192
+ from app.dependencies import get_current_user, get_db
193
+ from app.models.user import User
194
+ from app.schemas.user import UserResponse
195
+
196
+
197
+ router = APIRouter()
198
+
199
+
200
+ @router.get("/", response_model=list[UserResponse])
201
+ async def list_users(
202
+ limit: int = Query(default=50, ge=1, le=100),
203
+ offset: int = Query(default=0, ge=0),
204
+ db: AsyncSession = Depends(get_db),
205
+ current_user: User = Depends(get_current_user),
206
+ ):
207
+ result = await db.execute(
208
+ select(User).order_by(User.created_at.desc()).limit(limit).offset(offset)
209
+ )
210
+ return result.scalars().all()
211
+ ```
212
+
213
+ Use `httpx.AsyncClient` for external HTTP calls from async handlers. Do not call `requests` in an async route.
214
+
215
+ ## Error Handling
216
+
217
+ Centralize domain exceptions and keep response shapes stable.
218
+
219
+ ```python
220
+ from fastapi import FastAPI, Request
221
+ from fastapi.responses import JSONResponse
222
+
223
+
224
+ class ApiError(Exception):
225
+ def __init__(self, status_code: int, code: str, message: str):
226
+ self.status_code = status_code
227
+ self.code = code
228
+ self.message = message
229
+
230
+
231
+ def register_exception_handlers(app: FastAPI) -> None:
232
+ @app.exception_handler(ApiError)
233
+ async def api_error_handler(request: Request, exc: ApiError):
234
+ return JSONResponse(
235
+ status_code=exc.status_code,
236
+ content={"error": {"code": exc.code, "message": exc.message}},
237
+ )
238
+ ```
239
+
240
+ ## OpenAPI Customization
241
+
242
+ Assign the custom OpenAPI callable to `app.openapi`; do not just call the function once.
243
+
244
+ ```python
245
+ from fastapi import FastAPI
246
+ from fastapi.openapi.utils import get_openapi
247
+
248
+
249
+ def install_openapi(app: FastAPI) -> None:
250
+ def custom_openapi():
251
+ if app.openapi_schema:
252
+ return app.openapi_schema
253
+ app.openapi_schema = get_openapi(
254
+ title="Service API",
255
+ version="1.0.0",
256
+ routes=app.routes,
257
+ )
258
+ return app.openapi_schema
259
+
260
+ app.openapi = custom_openapi
261
+ ```
262
+
263
+ ## Testing
264
+
265
+ Override the dependency used by `Depends`, not an internal helper that route handlers never reference.
266
+
267
+ ```python
268
+ import pytest
269
+ from httpx import ASGITransport, AsyncClient
270
+ from sqlalchemy.ext.asyncio import AsyncSession
271
+
272
+ from app.dependencies import get_db
273
+ from app.main import create_app
274
+
275
+
276
+ @pytest.fixture
277
+ async def client(test_session: AsyncSession):
278
+ app = create_app()
279
+
280
+ async def override_get_db():
281
+ yield test_session
282
+
283
+ app.dependency_overrides[get_db] = override_get_db
284
+ async with AsyncClient(
285
+ transport=ASGITransport(app=app),
286
+ base_url="http://test",
287
+ ) as test_client:
288
+ yield test_client
289
+ app.dependency_overrides.clear()
290
+ ```
291
+
292
+ ## Security Checklist
293
+
294
+ - Hash passwords with `argon2-cffi`, `bcrypt`, or a current passlib-compatible hasher.
295
+ - Validate JWT issuer, audience, expiry, and signing algorithm.
296
+ - Keep CORS origins environment-specific.
297
+ - Put rate limits on auth and write-heavy endpoints.
298
+ - Use Pydantic models for all request bodies.
299
+ - Use ORM parameter binding or SQLAlchemy Core expressions; never build SQL with f-strings.
300
+ - Redact tokens, authorization headers, cookies, and passwords from logs.
301
+ - Run dependency audit tooling in CI.
302
+
303
+ ## Performance Checklist
304
+
305
+ - Configure database connection pooling explicitly.
306
+ - Add pagination to list endpoints.
307
+ - Watch for N+1 queries and use eager loading intentionally.
308
+ - Use async HTTP/database clients in async paths.
309
+ - Add compression only after checking payload size and CPU tradeoffs.
310
+ - Cache stable expensive reads behind explicit invalidation.
311
+
312
+ ## Examples
313
+
314
+ Use these examples as patterns, not as project-wide templates:
315
+
316
+ - Application factory: configure middleware and routers once in `create_app`.
317
+ - Schema split: `UserCreate`, `UserUpdate`, and `UserResponse` have different responsibilities.
318
+ - Dependency override: tests override `get_db` directly.
319
+ - OpenAPI customization: assign `app.openapi = custom_openapi`.
320
+
321
+ ## See Also
322
+
323
+ - Agent: `fastapi-reviewer`
324
+ - Command: `/fastapi-review`
325
+ - Skill: `python-patterns`
326
+ - Skill: `python-testing`
327
+ - Skill: `api-design`