Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when designing RESTful API endpoints in FastAPI or Python projects. Triggers for: creating GET/POST/PUT/DELETE endpoints, request validation with Pydantic, response formatting with JSON schemas, status code selection, pagination, filtering, or sorting parameters. NOT for: GraphQL APIs, WebSocket handlers, or non-RESTful endpoints.
.claude/skills/aiskillstore-api-route-design/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 3% | 0% |
| case-02 | ✗→✓ | ▲ Improved | -9% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 177% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-15 | ✓→✓ | = Same ✓ | 163% | 0% |
Expert design and implementation of RESTful APIs with proper validation, response formatting, and HTTP semantics.
| Pattern | Example | Purpose | |---------|---------|---------| | List resource | @router.get("/fees/", response_model=List[FeeOut]) | Retrieve collection | | Get by ID | @router.get("/fees/{fee_id}") | Retrieve single resource | | Create | @router.post("/fees/", response_model=FeeOut, status_code=201) | Create new resource | | Update | @router.put("/fees/{fee_id}") | Full resource update | | Patch | @router.patch("/fees/{fee_id}") | Partial resource update | | Delete | @router.delete("/fees/{fee_id}", status_code=204) | Remove resource |
/v1/{resource} # Collection endpoints
/v1/{resource}/{id} # Single resource endpoints
/v1/{resource}/{id}/sub # Nested resource endpointsRules:
/student-fees not /studentFees/users not /user| Code | Usage | Example | |------|-------|---------| | 200 | OK | Successful GET, PUT, PATCH | | 201 | Created | Successful POST (resource created) | | 202 | Accepted | Async operation started | | 204 | No Content | Successful DELETE | | 400 | Bad Request | Invalid input, validation failed | | 401 | Unauthorized | Missing or invalid auth | | 403 | Forbidden | Authenticated but not authorized | | 404 | Not Found | Resource doesn't exist | | 422 | Unprocessable Entity | Validation errors (Pydantic) | | 500 | Internal Server Error | Unexpected server error |
pythonfrom fastapi import APIRouter, HTTPException from typing import Annotated router = APIRouter() @router.get("/fees/{fee_id}") async def get_fee(fee_id: int): fee = await get_fee_by_id(fee_id) if not fee: raise HTTPException(status_code=404, detail="Fee not found") return fee
python@router.get("/fees/", response_model=List[FeeOut]) async def list_fees( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), status: str | None = Query(None, pattern="^(pending|paid|overdue)$"), sort_by: str = Query("created_at", enum=["created_at", "amount", "due_date"]), sort_order: str = Query("desc", enum=["asc", "desc"]), ): return await paginate_fees( skip=skip, limit=limit, status=status, sort_by=sort_by, sort_order=sort_order, )
pythonfrom pydantic import BaseModel from datetime import datetime class FeeCreate(BaseModel): student_id: int amount: float = Field(..., gt=0) due_date: datetime description: str | None = None class FeeUpdate(BaseModel): amount: float | None = Field(None, gt=0) status: str | None = Field(None, pattern="^(pending|paid|overdue)$") due_date: datetime | None = None @router.post("/fees/", response_model=FeeOut, status_code=201) async def create_fee(fee_in: FeeCreate): return await create_fee_db(fee_in) @router.patch("/fees/{fee_id}", response_model=FeeOut) async def update_fee(fee_id: int, fee_in: FeeUpdate): return await update_fee_db(fee_id, fee_in)
pythonclass FeeOut(BaseModel): id: int student_id: int amount: float status: str created_at: datetime due_date: datetime class PaginatedResponse(BaseModel): data: List[FeeOut] total: int skip: int limit: int has_more: bool
pythonclass ErrorResponse(BaseModel): error: str detail: str | None = None code: str | None = None
pythonfrom fastapi import APIRouter, Depends, HTTPException, Query, status from typing import List, Annotated router = APIRouter(prefix="/v1/fees", tags=["fees"]) @router.get( "/", response_model=PaginatedResponse[FeeOut], summary="List fees", description="Retrieve a paginated list of fees with optional filtering.", ) async def list_fees( skip: Annotated[int, Query(0, ge=0)] = 0, limit: Annotated[int, Query(100, ge=1, le=1000)] = 100, status: Annotated[str | None, Query(pattern="^(pending|paid|overdue)$")] = None, _current_user: User = Depends(get_current_user), ) -> PaginatedResponse[FeeOut]: fees, total = await get_fees( skip=skip, limit=limit, status=status, user=_current_user ) return PaginatedResponse( data=fees, total=total, skip=skip, limit=limit, has_more=(skip + limit) < total, ) @router.get( "/{fee_id}", response_model=FeeOut, responses={404: {"model": ErrorResponse}}, ) async def get_fee( fee_id: int, _current_user: User = Depends(get_current_user), ) -> FeeOut: fee = await get_fee_by_id(fee_id) if not fee: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Fee not found", ) return fee @router.post( "/", response_model=FeeOut, status_code=status.HTTP_201_CREATED, responses={400: {"model": ErrorResponse}}, ) async def create_fee( fee_in: FeeCreate, _current_user: User = Depends(get_current_user), ) -> FeeOut: return await create_fee_db(fee_in, created_by=_current_user.id)
| Skill | Integration Point | |-------|-------------------| | @fastapi-app | Router registration in main.py | | @sqlmodel-crud | Database operations in endpoints | | @jwt-auth | Depends(get_current_user) for protected routes | | @api-client | Consumer of this API design |
skip/limit with has_more indicatorsort_by and sort_order parametersresponse_modelsummary and description for OpenAPIpython@router.get("/items/", response_model=PaginatedResponse[ItemOut]) async def list_items( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), ) -> PaginatedResponse[ItemOut]: items, total = await get_items(skip=skip, limit=limit) return PaginatedResponse( data=items, total=total, skip=skip, limit=limit, has_more=(skip + limit) < total, )
python@router.get("/items/") async def list_items( # Filtering category: str | None = None, status: str | None = Query(None, pattern="^(active|inactive)$"), min_amount: float | None = Query(None, ge=0), # Sorting sort_by: str = Query("created_at", enum=["created_at", "amount", "name"]), sort_order: str = Query("desc", enum=["asc", "desc"]), ): return await get_items( filters={"category": category, "status": status, "min_amount": min_amount}, order_by=f"{sort_order} {sort_by.lstrip('-')}", )
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-15 | pass→pass | 15,185 | 14,267 | -6% | 1 | 1 | 0% | 1,935 | 5,094 | +163% | 0 | 0 | — |
case-01 | fail→pass | 33,785 | 24,227 | -28% | 1 | 1 | 0% | 6,238 | 6,422 | +3% | 0 | 0 | — |
case-02 | fail→pass | 29,544 | 22,055 | -25% | 1 | 1 | 0% | 6,546 | 5,937 | -9% | 0 | 0 | — |
case-03 | fail→fail | 25,495 | 23,571 | -8% | 1 | 1 | 0% | 4,517 | 6,457 | +43% | 0 | 0 | — |
case-04 | fail→fail | 14,704 | 21,947 | +49% | 1 | 1 | 0% | 3,165 | 5,660 | +79% | 0 | 0 | — |
case-05 | fail→pass | 7,715 | 16,324 | +112% | 1 | 1 | 0% | 1,600 | 4,432 | +177% | 0 | 0 | — |
case-06 | pass→pass | 13,840 | 10,651 | -23% | 1 | 1 | 0% | 1,870 | 4,538 | +143% | 0 | 0 | — |
case-07 | fail→fail | 13,443 | 18,700 | +39% | 1 | 1 | 0% | 2,843 | 4,590 | +61% | 0 | 0 | — |
case-08 | fail→fail | 10,812 | 9,830 | -9% | 1 | 1 | 0% | 1,968 | 4,094 | +108% | 0 | 0 | — |
case-09 | fail→fail | 15,056 | 19,298 | +28% | 1 | 1 | 0% | 1,927 | 4,822 | +150% | 0 | 0 | — |
case-10 | fail→fail | 8,719 | 17,079 | +96% | 1 | 1 | 0% | 1,578 | 4,577 | +190% | 0 | 0 | — |
case-11 | pass→pass | 11,604 | 7,434 | -36% | 1 | 1 | 0% | 2,373 | 3,763 | +59% | 0 | 0 | — |
case-12 | pass→pass | 20,627 | 12,304 | -40% | 1 | 1 | 0% | 1,610 | 3,779 | +135% | 0 | 0 | — |
case-13 | pass→pass | 15,137 | 11,893 | -21% | 1 | 1 | 0% | 1,641 | 3,613 | +120% | 0 | 0 | — |
case-14 | fail→pass | 18,177 | 17,759 | -2% | 1 | 1 | 0% | 2,643 | 4,875 | +84% | 0 | 0 | — |
case-16 | pass→pass | 18,727 | 14,703 | -21% | 1 | 1 | 0% | 2,726 | 4,174 | +53% | 0 | 0 | — |
case-17 | pass→pass | 11,021 | 20,509 | +86% | 1 | 1 | 0% | 2,035 | 3,968 | +95% | 0 | 0 | — |
case-18 | pass→pass | 10,263 | 10,635 | +4% | 1 | 1 | 0% | 2,068 | 3,361 | +63% | 0 | 0 | — |
case-19 | pass→pass | 16,889 | 13,135 | -22% | 1 | 1 | 0% | 2,065 | 4,012 | +94% | 0 | 0 | — |
case-20 | pass→pass | 14,791 | 13,633 | -8% | 1 | 1 | 0% | 1,749 | 3,664 | +109% | 0 | 0 | — |
case-21 | pass→pass | 16,203 | 17,260 | +7% | 1 | 1 | 0% | 2,364 | 4,911 | +108% | 0 | 0 | — |
case-22 | pass→pass | 14,825 | 9,410 | -37% | 1 | 1 | 0% | 1,906 | 4,208 | +121% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.