autoworkflow 3.1.5 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# FastAPI Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
app/
|
|
6
|
+
├── main.py # FastAPI app
|
|
7
|
+
├── config.py # Settings
|
|
8
|
+
├── database.py # DB connection
|
|
9
|
+
├── dependencies.py # Shared dependencies
|
|
10
|
+
├── models/ # SQLAlchemy models
|
|
11
|
+
│ └── user.py
|
|
12
|
+
├── schemas/ # Pydantic schemas
|
|
13
|
+
│ └── user.py
|
|
14
|
+
├── routers/ # API routes
|
|
15
|
+
│ ├── __init__.py
|
|
16
|
+
│ └── users.py
|
|
17
|
+
├── services/ # Business logic
|
|
18
|
+
│ └── user.py
|
|
19
|
+
└── tests/
|
|
20
|
+
└── test_users.py
|
|
21
|
+
\`\`\`
|
|
22
|
+
|
|
23
|
+
## App Setup
|
|
24
|
+
\`\`\`python
|
|
25
|
+
# main.py
|
|
26
|
+
from fastapi import FastAPI
|
|
27
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
28
|
+
from contextlib import asynccontextmanager
|
|
29
|
+
|
|
30
|
+
from app.database import engine, Base
|
|
31
|
+
from app.routers import users, auth
|
|
32
|
+
|
|
33
|
+
@asynccontextmanager
|
|
34
|
+
async def lifespan(app: FastAPI):
|
|
35
|
+
# Startup
|
|
36
|
+
async with engine.begin() as conn:
|
|
37
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
38
|
+
yield
|
|
39
|
+
# Shutdown
|
|
40
|
+
await engine.dispose()
|
|
41
|
+
|
|
42
|
+
app = FastAPI(
|
|
43
|
+
title="My API",
|
|
44
|
+
version="1.0.0",
|
|
45
|
+
lifespan=lifespan,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# CORS
|
|
49
|
+
app.add_middleware(
|
|
50
|
+
CORSMiddleware,
|
|
51
|
+
allow_origins=["http://localhost:3000"],
|
|
52
|
+
allow_credentials=True,
|
|
53
|
+
allow_methods=["*"],
|
|
54
|
+
allow_headers=["*"],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Routes
|
|
58
|
+
app.include_router(auth.router, prefix="/auth", tags=["auth"])
|
|
59
|
+
app.include_router(users.router, prefix="/users", tags=["users"])
|
|
60
|
+
|
|
61
|
+
@app.get("/health")
|
|
62
|
+
async def health():
|
|
63
|
+
return {"status": "ok"}
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
## Schemas (Pydantic)
|
|
67
|
+
\`\`\`python
|
|
68
|
+
# schemas/user.py
|
|
69
|
+
from pydantic import BaseModel, EmailStr, Field
|
|
70
|
+
from datetime import datetime
|
|
71
|
+
|
|
72
|
+
class UserBase(BaseModel):
|
|
73
|
+
email: EmailStr
|
|
74
|
+
name: str = Field(min_length=1, max_length=100)
|
|
75
|
+
|
|
76
|
+
class UserCreate(UserBase):
|
|
77
|
+
password: str = Field(min_length=8)
|
|
78
|
+
|
|
79
|
+
class UserUpdate(BaseModel):
|
|
80
|
+
name: str | None = None
|
|
81
|
+
bio: str | None = None
|
|
82
|
+
|
|
83
|
+
class UserResponse(UserBase):
|
|
84
|
+
id: int
|
|
85
|
+
created_at: datetime
|
|
86
|
+
|
|
87
|
+
model_config = {"from_attributes": True}
|
|
88
|
+
|
|
89
|
+
class UserWithPosts(UserResponse):
|
|
90
|
+
posts: list["PostResponse"] = []
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
## Routes
|
|
94
|
+
\`\`\`python
|
|
95
|
+
# routers/users.py
|
|
96
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
97
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
98
|
+
|
|
99
|
+
from app.database import get_db
|
|
100
|
+
from app.schemas.user import UserCreate, UserResponse, UserUpdate
|
|
101
|
+
from app.services.user import UserService
|
|
102
|
+
from app.dependencies import get_current_user
|
|
103
|
+
|
|
104
|
+
router = APIRouter()
|
|
105
|
+
|
|
106
|
+
@router.get("/", response_model=list[UserResponse])
|
|
107
|
+
async def list_users(
|
|
108
|
+
skip: int = Query(0, ge=0),
|
|
109
|
+
limit: int = Query(10, ge=1, le=100),
|
|
110
|
+
db: AsyncSession = Depends(get_db),
|
|
111
|
+
):
|
|
112
|
+
service = UserService(db)
|
|
113
|
+
return await service.get_users(skip=skip, limit=limit)
|
|
114
|
+
|
|
115
|
+
@router.get("/me", response_model=UserResponse)
|
|
116
|
+
async def get_me(current_user: User = Depends(get_current_user)):
|
|
117
|
+
return current_user
|
|
118
|
+
|
|
119
|
+
@router.get("/{user_id}", response_model=UserResponse)
|
|
120
|
+
async def get_user(
|
|
121
|
+
user_id: int,
|
|
122
|
+
db: AsyncSession = Depends(get_db),
|
|
123
|
+
):
|
|
124
|
+
service = UserService(db)
|
|
125
|
+
user = await service.get_user(user_id)
|
|
126
|
+
if not user:
|
|
127
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
128
|
+
return user
|
|
129
|
+
|
|
130
|
+
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
|
131
|
+
async def create_user(
|
|
132
|
+
user_data: UserCreate,
|
|
133
|
+
db: AsyncSession = Depends(get_db),
|
|
134
|
+
):
|
|
135
|
+
service = UserService(db)
|
|
136
|
+
return await service.create_user(user_data)
|
|
137
|
+
|
|
138
|
+
@router.patch("/{user_id}", response_model=UserResponse)
|
|
139
|
+
async def update_user(
|
|
140
|
+
user_id: int,
|
|
141
|
+
user_data: UserUpdate,
|
|
142
|
+
db: AsyncSession = Depends(get_db),
|
|
143
|
+
current_user: User = Depends(get_current_user),
|
|
144
|
+
):
|
|
145
|
+
if current_user.id != user_id:
|
|
146
|
+
raise HTTPException(status_code=403, detail="Not authorized")
|
|
147
|
+
service = UserService(db)
|
|
148
|
+
return await service.update_user(user_id, user_data)
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
## Dependency Injection
|
|
152
|
+
\`\`\`python
|
|
153
|
+
# dependencies.py
|
|
154
|
+
from fastapi import Depends, HTTPException, status
|
|
155
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
156
|
+
from jose import JWTError, jwt
|
|
157
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
158
|
+
|
|
159
|
+
from app.database import get_db
|
|
160
|
+
from app.config import settings
|
|
161
|
+
|
|
162
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
|
|
163
|
+
|
|
164
|
+
async def get_current_user(
|
|
165
|
+
token: str = Depends(oauth2_scheme),
|
|
166
|
+
db: AsyncSession = Depends(get_db),
|
|
167
|
+
) -> User:
|
|
168
|
+
credentials_exception = HTTPException(
|
|
169
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
170
|
+
detail="Could not validate credentials",
|
|
171
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
payload = jwt.decode(token, settings.jwt_secret, algorithms=["HS256"])
|
|
176
|
+
user_id: str = payload.get("sub")
|
|
177
|
+
if user_id is None:
|
|
178
|
+
raise credentials_exception
|
|
179
|
+
except JWTError:
|
|
180
|
+
raise credentials_exception
|
|
181
|
+
|
|
182
|
+
user = await db.get(User, int(user_id))
|
|
183
|
+
if user is None:
|
|
184
|
+
raise credentials_exception
|
|
185
|
+
return user
|
|
186
|
+
|
|
187
|
+
# Optional auth
|
|
188
|
+
async def get_current_user_optional(
|
|
189
|
+
token: str | None = Depends(oauth2_scheme),
|
|
190
|
+
db: AsyncSession = Depends(get_db),
|
|
191
|
+
) -> User | None:
|
|
192
|
+
if not token:
|
|
193
|
+
return None
|
|
194
|
+
try:
|
|
195
|
+
return await get_current_user(token, db)
|
|
196
|
+
except HTTPException:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# Role-based access
|
|
200
|
+
def require_role(role: str):
|
|
201
|
+
async def role_checker(user: User = Depends(get_current_user)):
|
|
202
|
+
if user.role != role:
|
|
203
|
+
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
|
204
|
+
return user
|
|
205
|
+
return role_checker
|
|
206
|
+
|
|
207
|
+
# Usage
|
|
208
|
+
@router.delete("/{user_id}")
|
|
209
|
+
async def delete_user(
|
|
210
|
+
user_id: int,
|
|
211
|
+
admin: User = Depends(require_role("admin")),
|
|
212
|
+
):
|
|
213
|
+
...
|
|
214
|
+
\`\`\`
|
|
215
|
+
|
|
216
|
+
## Authentication (OAuth2 + JWT)
|
|
217
|
+
\`\`\`python
|
|
218
|
+
# routers/auth.py
|
|
219
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
220
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
|
221
|
+
from datetime import datetime, timedelta
|
|
222
|
+
|
|
223
|
+
router = APIRouter()
|
|
224
|
+
|
|
225
|
+
@router.post("/token")
|
|
226
|
+
async def login(
|
|
227
|
+
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
228
|
+
db: AsyncSession = Depends(get_db),
|
|
229
|
+
):
|
|
230
|
+
user = await authenticate_user(db, form_data.username, form_data.password)
|
|
231
|
+
if not user:
|
|
232
|
+
raise HTTPException(
|
|
233
|
+
status_code=401,
|
|
234
|
+
detail="Incorrect email or password",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
access_token = create_access_token(
|
|
238
|
+
data={"sub": str(user.id)},
|
|
239
|
+
expires_delta=timedelta(hours=1),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"access_token": access_token,
|
|
244
|
+
"token_type": "bearer",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def create_access_token(data: dict, expires_delta: timedelta) -> str:
|
|
248
|
+
expire = datetime.utcnow() + expires_delta
|
|
249
|
+
to_encode = data.copy()
|
|
250
|
+
to_encode.update({"exp": expire})
|
|
251
|
+
return jwt.encode(to_encode, settings.jwt_secret, algorithm="HS256")
|
|
252
|
+
\`\`\`
|
|
253
|
+
|
|
254
|
+
## Error Handling
|
|
255
|
+
\`\`\`python
|
|
256
|
+
from fastapi import FastAPI, Request
|
|
257
|
+
from fastapi.responses import JSONResponse
|
|
258
|
+
|
|
259
|
+
class AppException(Exception):
|
|
260
|
+
def __init__(self, status_code: int, detail: str):
|
|
261
|
+
self.status_code = status_code
|
|
262
|
+
self.detail = detail
|
|
263
|
+
|
|
264
|
+
@app.exception_handler(AppException)
|
|
265
|
+
async def app_exception_handler(request: Request, exc: AppException):
|
|
266
|
+
return JSONResponse(
|
|
267
|
+
status_code=exc.status_code,
|
|
268
|
+
content={"error": exc.detail},
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@app.exception_handler(Exception)
|
|
272
|
+
async def global_exception_handler(request: Request, exc: Exception):
|
|
273
|
+
return JSONResponse(
|
|
274
|
+
status_code=500,
|
|
275
|
+
content={"error": "Internal server error"},
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Usage
|
|
279
|
+
raise AppException(status_code=400, detail="Invalid input")
|
|
280
|
+
\`\`\`
|
|
281
|
+
|
|
282
|
+
## Background Tasks
|
|
283
|
+
\`\`\`python
|
|
284
|
+
from fastapi import BackgroundTasks
|
|
285
|
+
|
|
286
|
+
async def send_email(email: str, message: str):
|
|
287
|
+
# Simulated email sending
|
|
288
|
+
await asyncio.sleep(5)
|
|
289
|
+
print(f"Email sent to {email}")
|
|
290
|
+
|
|
291
|
+
@router.post("/users/")
|
|
292
|
+
async def create_user(
|
|
293
|
+
user: UserCreate,
|
|
294
|
+
background_tasks: BackgroundTasks,
|
|
295
|
+
db: AsyncSession = Depends(get_db),
|
|
296
|
+
):
|
|
297
|
+
new_user = await service.create_user(user)
|
|
298
|
+
background_tasks.add_task(send_email, new_user.email, "Welcome!")
|
|
299
|
+
return new_user
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
## Middleware
|
|
303
|
+
\`\`\`python
|
|
304
|
+
from fastapi import Request
|
|
305
|
+
import time
|
|
306
|
+
|
|
307
|
+
@app.middleware("http")
|
|
308
|
+
async def add_timing_header(request: Request, call_next):
|
|
309
|
+
start_time = time.time()
|
|
310
|
+
response = await call_next(request)
|
|
311
|
+
duration = time.time() - start_time
|
|
312
|
+
response.headers["X-Response-Time"] = f"{duration:.3f}s"
|
|
313
|
+
return response
|
|
314
|
+
\`\`\`
|
|
315
|
+
|
|
316
|
+
## ❌ DON'T
|
|
317
|
+
- Use sync operations in async routes
|
|
318
|
+
- Skip response_model (loses validation)
|
|
319
|
+
- Put DB logic in route handlers
|
|
320
|
+
- Forget to handle exceptions
|
|
321
|
+
|
|
322
|
+
## ✅ DO
|
|
323
|
+
- Always define response_model
|
|
324
|
+
- Use Depends() for shared logic
|
|
325
|
+
- Use async for I/O operations
|
|
326
|
+
- Use services for business logic
|
|
327
|
+
- Use OAuth2 + JWT for auth
|
|
328
|
+
- Use background tasks for slow operations
|
|
329
|
+
- Handle errors with exception handlers
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Fastify Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
src/
|
|
6
|
+
├── index.ts # Entry point
|
|
7
|
+
├── app.ts # Fastify app factory
|
|
8
|
+
├── plugins/
|
|
9
|
+
│ ├── auth.ts # Auth plugin
|
|
10
|
+
│ └── database.ts # DB connection
|
|
11
|
+
├── routes/
|
|
12
|
+
│ ├── index.ts # Route registration
|
|
13
|
+
│ └── users/
|
|
14
|
+
│ ├── index.ts # User routes
|
|
15
|
+
│ └── schemas.ts # Route schemas
|
|
16
|
+
└── types/
|
|
17
|
+
└── index.d.ts # Type extensions
|
|
18
|
+
\`\`\`
|
|
19
|
+
|
|
20
|
+
## App Setup with TypeBox
|
|
21
|
+
\`\`\`typescript
|
|
22
|
+
import Fastify from 'fastify';
|
|
23
|
+
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
|
|
24
|
+
import cors from '@fastify/cors';
|
|
25
|
+
import helmet from '@fastify/helmet';
|
|
26
|
+
|
|
27
|
+
export async function buildApp() {
|
|
28
|
+
const app = Fastify({
|
|
29
|
+
logger: {
|
|
30
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
31
|
+
},
|
|
32
|
+
}).withTypeProvider<TypeBoxTypeProvider>();
|
|
33
|
+
|
|
34
|
+
// Plugins
|
|
35
|
+
await app.register(cors, { origin: true });
|
|
36
|
+
await app.register(helmet);
|
|
37
|
+
|
|
38
|
+
// Custom plugins
|
|
39
|
+
await app.register(import('./plugins/database'));
|
|
40
|
+
await app.register(import('./plugins/auth'));
|
|
41
|
+
|
|
42
|
+
// Routes
|
|
43
|
+
await app.register(import('./routes'), { prefix: '/api' });
|
|
44
|
+
|
|
45
|
+
return app;
|
|
46
|
+
}
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
## Routes with Type Safety
|
|
50
|
+
\`\`\`typescript
|
|
51
|
+
// routes/users/index.ts
|
|
52
|
+
import { FastifyPluginAsync } from 'fastify';
|
|
53
|
+
import { Type, Static } from '@sinclair/typebox';
|
|
54
|
+
|
|
55
|
+
// Define schemas with TypeBox
|
|
56
|
+
const UserSchema = Type.Object({
|
|
57
|
+
id: Type.String(),
|
|
58
|
+
email: Type.String({ format: 'email' }),
|
|
59
|
+
name: Type.String(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const CreateUserSchema = Type.Object({
|
|
63
|
+
email: Type.String({ format: 'email' }),
|
|
64
|
+
name: Type.String({ minLength: 1 }),
|
|
65
|
+
password: Type.String({ minLength: 8 }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const ParamsSchema = Type.Object({
|
|
69
|
+
id: Type.String(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
type User = Static<typeof UserSchema>;
|
|
73
|
+
type CreateUser = Static<typeof CreateUserSchema>;
|
|
74
|
+
|
|
75
|
+
const usersRoutes: FastifyPluginAsync = async (app) => {
|
|
76
|
+
// GET /users
|
|
77
|
+
app.get('/', {
|
|
78
|
+
schema: {
|
|
79
|
+
response: {
|
|
80
|
+
200: Type.Array(UserSchema),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}, async (request, reply) => {
|
|
84
|
+
const users = await app.db.user.findMany();
|
|
85
|
+
return users;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// GET /users/:id
|
|
89
|
+
app.get<{ Params: Static<typeof ParamsSchema> }>('/:id', {
|
|
90
|
+
schema: {
|
|
91
|
+
params: ParamsSchema,
|
|
92
|
+
response: {
|
|
93
|
+
200: UserSchema,
|
|
94
|
+
404: Type.Object({ error: Type.String() }),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}, async (request, reply) => {
|
|
98
|
+
const user = await app.db.user.findUnique({
|
|
99
|
+
where: { id: request.params.id },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!user) {
|
|
103
|
+
return reply.status(404).send({ error: 'User not found' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return user;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// POST /users
|
|
110
|
+
app.post<{ Body: CreateUser }>('/', {
|
|
111
|
+
schema: {
|
|
112
|
+
body: CreateUserSchema,
|
|
113
|
+
response: {
|
|
114
|
+
201: UserSchema,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}, async (request, reply) => {
|
|
118
|
+
const user = await app.db.user.create({
|
|
119
|
+
data: request.body,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return reply.status(201).send(user);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default usersRoutes;
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
## Plugins
|
|
130
|
+
\`\`\`typescript
|
|
131
|
+
// plugins/database.ts
|
|
132
|
+
import { FastifyPluginAsync } from 'fastify';
|
|
133
|
+
import fp from 'fastify-plugin';
|
|
134
|
+
import { PrismaClient } from '@prisma/client';
|
|
135
|
+
|
|
136
|
+
declare module 'fastify' {
|
|
137
|
+
interface FastifyInstance {
|
|
138
|
+
db: PrismaClient;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const databasePlugin: FastifyPluginAsync = async (app) => {
|
|
143
|
+
const prisma = new PrismaClient();
|
|
144
|
+
|
|
145
|
+
await prisma.$connect();
|
|
146
|
+
|
|
147
|
+
app.decorate('db', prisma);
|
|
148
|
+
|
|
149
|
+
app.addHook('onClose', async () => {
|
|
150
|
+
await prisma.$disconnect();
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Use fp() to make decorators available to sibling plugins
|
|
155
|
+
export default fp(databasePlugin, { name: 'database' });
|
|
156
|
+
\`\`\`
|
|
157
|
+
|
|
158
|
+
## Authentication Plugin
|
|
159
|
+
\`\`\`typescript
|
|
160
|
+
// plugins/auth.ts
|
|
161
|
+
import { FastifyPluginAsync, FastifyRequest } from 'fastify';
|
|
162
|
+
import fp from 'fastify-plugin';
|
|
163
|
+
import jwt from '@fastify/jwt';
|
|
164
|
+
|
|
165
|
+
declare module 'fastify' {
|
|
166
|
+
interface FastifyInstance {
|
|
167
|
+
authenticate: (request: FastifyRequest) => Promise<void>;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
declare module '@fastify/jwt' {
|
|
172
|
+
interface FastifyJWT {
|
|
173
|
+
payload: { id: string; email: string };
|
|
174
|
+
user: { id: string; email: string };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const authPlugin: FastifyPluginAsync = async (app) => {
|
|
179
|
+
await app.register(jwt, {
|
|
180
|
+
secret: process.env.JWT_SECRET!,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
app.decorate('authenticate', async (request: FastifyRequest) => {
|
|
184
|
+
await request.jwtVerify();
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default fp(authPlugin, { name: 'auth' });
|
|
189
|
+
|
|
190
|
+
// Usage in routes
|
|
191
|
+
app.get('/me', {
|
|
192
|
+
preHandler: [app.authenticate],
|
|
193
|
+
}, async (request) => {
|
|
194
|
+
return request.user;
|
|
195
|
+
});
|
|
196
|
+
\`\`\`
|
|
197
|
+
|
|
198
|
+
## Hooks
|
|
199
|
+
\`\`\`typescript
|
|
200
|
+
// Lifecycle hooks
|
|
201
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
202
|
+
request.startTime = Date.now();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
app.addHook('onResponse', async (request, reply) => {
|
|
206
|
+
const duration = Date.now() - request.startTime;
|
|
207
|
+
request.log.info({ duration }, 'Request completed');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Route-level hooks
|
|
211
|
+
app.get('/protected', {
|
|
212
|
+
preHandler: async (request, reply) => {
|
|
213
|
+
// Auth check before handler
|
|
214
|
+
if (!request.headers.authorization) {
|
|
215
|
+
return reply.status(401).send({ error: 'Unauthorized' });
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
}, async (request) => {
|
|
219
|
+
return { data: 'secret' };
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Validation hook (runs after schema validation)
|
|
223
|
+
app.addHook('preValidation', async (request) => {
|
|
224
|
+
// Modify request before validation
|
|
225
|
+
});
|
|
226
|
+
\`\`\`
|
|
227
|
+
|
|
228
|
+
## Error Handling
|
|
229
|
+
\`\`\`typescript
|
|
230
|
+
// Custom error handler
|
|
231
|
+
app.setErrorHandler((error, request, reply) => {
|
|
232
|
+
request.log.error(error);
|
|
233
|
+
|
|
234
|
+
// Validation errors
|
|
235
|
+
if (error.validation) {
|
|
236
|
+
return reply.status(400).send({
|
|
237
|
+
error: 'Validation failed',
|
|
238
|
+
details: error.validation,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Custom errors
|
|
243
|
+
if (error.statusCode) {
|
|
244
|
+
return reply.status(error.statusCode).send({
|
|
245
|
+
error: error.message,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Generic errors
|
|
250
|
+
reply.status(500).send({
|
|
251
|
+
error: 'Internal server error',
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// 404 handler
|
|
256
|
+
app.setNotFoundHandler((request, reply) => {
|
|
257
|
+
reply.status(404).send({
|
|
258
|
+
error: 'Not found',
|
|
259
|
+
path: request.url,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
## Decorators
|
|
265
|
+
\`\`\`typescript
|
|
266
|
+
// Add to FastifyInstance
|
|
267
|
+
app.decorate('config', {
|
|
268
|
+
apiVersion: '1.0.0',
|
|
269
|
+
environment: process.env.NODE_ENV,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Add to FastifyRequest
|
|
273
|
+
app.decorateRequest('startTime', 0);
|
|
274
|
+
|
|
275
|
+
// Add to FastifyReply
|
|
276
|
+
app.decorateReply('success', function (data: any) {
|
|
277
|
+
return this.send({ success: true, data });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Usage
|
|
281
|
+
app.get('/health', async (request, reply) => {
|
|
282
|
+
return reply.success({ status: 'ok', version: app.config.apiVersion });
|
|
283
|
+
});
|
|
284
|
+
\`\`\`
|
|
285
|
+
|
|
286
|
+
## ❌ DON'T
|
|
287
|
+
- Forget to use fp() for plugins with decorators
|
|
288
|
+
- Skip schema validation (it's a key Fastify feature)
|
|
289
|
+
- Block the event loop in handlers
|
|
290
|
+
- Ignore the logger (use request.log)
|
|
291
|
+
|
|
292
|
+
## ✅ DO
|
|
293
|
+
- Use TypeBox for type-safe schemas
|
|
294
|
+
- Use plugins for modularity
|
|
295
|
+
- Enable logging (built-in pino)
|
|
296
|
+
- Use hooks for cross-cutting concerns
|
|
297
|
+
- Use fp() for plugins that add decorators
|
|
298
|
+
- Handle errors with setErrorHandler
|
|
299
|
+
- Use preHandler for auth/validation
|