Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when creating FastAPI backend applications - route handlers, dependencies, CORS config, or Pydantic models. NOT when frontend logic, non-Python backends, or unrelated server-side code. Triggers: "FastAPI", "student endpoint", "API route", "dependency injection", "CORS", "Pydantic model".
.claude/skills/aiskillstore-fastapi-app/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-21 | ✗→✓ | ▲ Improved | 157% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 167% | 0% |
| case-23 | ✗→✓ | ▲ Improved | 165% | 0% |
| case-09 | ✓→✗ | ▼ Worse | 331% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 192% | 0% |
Expert guidance for building FastAPI backend applications with route decorators, dependency injection, CORS configuration, and Pydantic v2 validation. Supports ERP endpoints for students, fees, attendance, and authentication.
This skill triggers when users request:
python# main.py from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from routers import students, fees, attendance, auth from dependencies.database import get_db from dependencies.auth import get_current_user import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): # Startup logger.info("Starting up FastAPI application...") yield # Shutdown logger.info("Shutting down FastAPI application...") app = FastAPI( title="ERP API", description="Educational Resource Planning API", version="1.0.0", lifespan=lifespan, docs_url="/docs", redoc_url="/redoc", ) # CORS Configuration app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000", "http://localhost:5173"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include routers with prefix and tags app.include_router( auth.router, prefix="/api/v1/auth", tags=["Authentication"], ) app.include_router( students.router, prefix="/api/v1/students", tags=["Students"], dependencies=[Depends(get_current_user)], ) app.include_router( fees.router, prefix="/api/v1/fees", tags=["Fees"], dependencies=[Depends(get_current_user)], ) app.include_router( attendance.router, prefix="/api/v1/attendance", tags=["Attendance"], dependencies=[Depends(get_current_user)], )
Requirements:
python# routers/students.py from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from typing import List, Optional from pydantic import BaseModel, EmailStr, Field from dependencies.database import get_db from dependencies.auth import get_current_user, get_admin_user from models.student import Student as StudentModel from schemas.student import StudentCreate, StudentUpdate, StudentResponse router = APIRouter() @router.get("/", response_model=List[StudentResponse]) async def get_students( skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user), ): """Get all students with pagination""" students = await db.execute( select(StudentModel) .offset(skip) .limit(limit) ) return students.scalars().all() @router.get("/{student_id}", response_model=StudentResponse) async def get_student( student_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user), ): """Get a single student by ID""" student = await db.execute( select(StudentModel).where(StudentModel.id == student_id) ) student = student.scalar_one_or_none() if not student: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Student not found" ) return student @router.post("/", response_model=StudentResponse, status_code=status.HTTP_201_CREATED) async def create_student( student_data: StudentCreate, db: AsyncSession = Depends(get_db), current_user = Depends(get_admin_user), ): """Create a new student""" # Check if email exists existing = await db.execute( select(StudentModel).where(StudentModel.email == student_data.email) ) if existing.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" ) student = StudentModel(**student_data.model_dump()) db.add(student) await db.commit() await db.refresh(student) return student @router.put("/{student_id}", response_model=StudentResponse) async def update_student( student_id: str, student_data: StudentUpdate, db: AsyncSession = Depends(get_db), current_user = Depends(get_admin_user), ): """Update a student""" student = await db.execute( select(StudentModel).where(StudentModel.id == student_id) ) student = student.scalar_one_or_none() if not student: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Student not found" ) update_data = student_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(student, field, value) await db.commit() await db.refresh(student) return student @router.delete("/{student_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_student( student_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_admin_user), ): """Delete a student""" student = await db.execute( select(StudentModel).where(StudentModel.id == student_id) ) student = student.scalar_one_or_none() if not student: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Student not found" ) await db.delete(student) await db.commit()
Requirements:
python# dependencies/database.py from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from databases import Database import os DATABASE_URL = os.getenv( "DATABASE_URL", "postgresql+asyncpg://user:password@localhost:5432/erp_db" ) engine = create_async_engine(DATABASE_URL, echo=True) async_session_maker = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False, ) async def get_db() -> AsyncSession: async with async_session_maker() as session: try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() async def init_db(): """Initialize database tables""" async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all)
python# dependencies/auth.py from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jose import JWTError, jwt from datetime import datetime, timedelta from typing import Optional import os SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 security = HTTPBearer() def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), ): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode( credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM] ) user_id: str = payload.get("sub") if user_id is None: raise credentials_exception except JWTError: raise credentials_exception return {"user_id": user_id, "role": payload.get("role")} async def get_admin_user(current_user = Depends(get_current_user)): if current_user.get("role") not in ["admin", "teacher"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required" ) return current_user
Requirements:
python# schemas/student.py from pydantic import BaseModel, EmailStr, Field, ConfigDict from datetime import datetime from typing import Optional from enum import Enum class StudentRole(str, Enum): STUDENT = "student" TEACHER = "teacher" ADMIN = "admin" # Base model with config class StudentBase(BaseModel): model_config = ConfigDict(from_attributes=True) name: str = Field(..., min_length=2, max_length=100) email: EmailStr phone: Optional[str] = Field(None, pattern=r'^\+?[\d\s-]+$') # Create schema class StudentCreate(StudentBase): password: str = Field(..., min_length=8) class_id: Optional[str] = None # Update schema (partial updates) class StudentUpdate(BaseModel): model_config = ConfigDict(from_attributes=True) name: Optional[str] = Field(None, min_length=2, max_length=100) email: Optional[EmailStr] = None phone: Optional[str] = None class_id: Optional[str] = None # Response schema class StudentResponse(StudentBase): id: str class_id: Optional[str] = None created_at: datetime updated_at: datetime # Pagination response class PaginatedResponse(BaseModel): model_config = ConfigDict(from_attributes=True) data: list[StudentResponse] meta: dict = { "total": 0, "page": 1, "page_size": 100, "total_pages": 1 }
python# schemas/fees.py from pydantic import BaseModel, Field from datetime import datetime from typing import Optional from enum import Enum class FeeStatus(str, Enum): PENDING = "pending" PAID = "paid" OVERDUE = "overdue" WAIVED = "waived" class FeeBase(BaseModel): student_id: str amount: float = Field(..., gt=0) description: str = Field(..., max_length=500) due_date: datetime class FeeCreate(FeeBase): pass class FeeUpdate(BaseModel): status: Optional[FeeStatus] = None paid_date: Optional[datetime] = None notes: Optional[str] = None class FeeResponse(FeeBase): id: str status: FeeStatus paid_date: Optional[datetime] = None created_at: datetime
Requirements:
main.py - FastAPI app initializationconfig.py - Settings and environment variablesrouters/__init__.pyrouters/students.pyrouters/fees.pyrouters/attendance.pyrouters/auth.pydependencies/__init__.pydependencies/database.pydependencies/auth.pymodels/__init__.pymodels/student.pyschemas/__init__.pyschemas/student.pyschemas/fees.pyBefore completing any FastAPI implementation:
python# routers/students.py @router.get("/students/", response_model=List[StudentResponse]) async def get_students( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), class_id: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user), ): """Get students with pagination and optional class filter""" query = select(StudentModel) if class_id: query = query.where(StudentModel.class_id == class_id) query = query.offset(skip).limit(limit).order_by(StudentModel.created_at.desc()) result = await db.execute(query) return result.scalars().all()
python# main.py from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:3000", # Next.js dev "http://localhost:5173", # Vite dev "https://yourdomain.com", # Production ], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"], allow_headers=["Authorization", "Content-Type"], )
python# dependencies/database.py async def get_db() -> AsyncSession: async with async_session_maker() as session: try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close()
python# dependencies/auth.py from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer oauth2_scheme = HTTPBearer() async def get_token_data(token: str = Depends(oauth2_scheme)): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) return payload except JWTError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token" )
python# dependencies/rate_limit.py from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) def rate_limit(requests: int = 60, seconds: int = 60): def decorator(func): return limiter.limit(f"{requests}/{seconds}")(func) return decorator # Usage @router.get("/students/") @rate_limit(requests=100, seconds=60) async def get_students(): return {"message": "Students list"}
python# config.py from pydantic_settings import BaseSettings from typing import List class Settings(BaseSettings): APP_NAME: str = "ERP API" DEBUG: bool = False API_V1_PREFIX: str = "/api/v1" # Database DATABASE_URL: str = "postgresql+asyncpg://user:password@localhost:5432/erp_db" # JWT JWT_SECRET_KEY: str JWT_ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 # CORS CORS_ORIGINS: List[str] = ["http://localhost:3000"] class Config: env_file = ".env" env_file_encoding = "utf-8" settings = Settings()
bash# Install dependencies pip install fastapi uvicorn[standard] sqlalchemy[asyncio] asyncpg pip install python-jose[cryptography] passlib[bcrypt] pip install slowapi pydantic-settings # Run development uvicorn main:app --reload --host 0.0.0.0 --port 8000 # Run production uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 40,258 | 35,801 | -11% | 1 | 1 | 0% | 8,260 | 12,432 | +51% | 0 | 0 | — |
case-02 | fail→fail | 29,009 | 31,207 | +8% | 1 | 1 | 0% | 5,270 | 12,161 | +131% | 0 | 0 | — |
case-03 | fail→fail | 25,450 | 21,370 | -16% | 1 | 1 | 0% | 5,256 | 9,745 | +85% | 0 | 0 | — |
case-04 | pass→pass | 21,516 | 21,568 | +0% | 1 | 1 | 0% | 2,897 | 8,452 | +192% | 0 | 0 | — |
case-05 | pass→pass | 18,052 | 23,438 | +30% | 1 | 1 | 0% | 2,686 | 8,689 | +223% | 0 | 0 | — |
case-06 | pass→pass | 18,701 | 14,128 | -24% | 1 | 1 | 0% | 2,784 | 8,050 | +189% | 0 | 0 | — |
case-07 | pass→pass | 15,868 | 16,041 | +1% | 1 | 1 | 0% | 1,932 | 6,945 | +259% | 0 | 0 | — |
case-08 | pass→pass | 15,162 | 7,779 | -49% | 1 | 1 | 0% | 1,922 | 6,460 | +236% | 0 | 0 | — |
case-09 | pass→fail | 13,895 | 11,151 | -20% | 1 | 1 | 0% | 1,670 | 7,196 | +331% | 0 | 0 | — |
case-10 | pass→pass | 10,241 | 11,463 | +12% | 1 | 1 | 0% | 2,019 | 7,268 | +260% | 0 | 0 | — |
case-11 | pass→pass | 14,529 | 6,902 | -52% | 1 | 1 | 0% | 1,812 | 6,385 | +252% | 0 | 0 | — |
case-12 | pass→pass | 16,902 | 10,497 | -38% | 1 | 1 | 0% | 2,196 | 7,019 | +220% | 0 | 0 | — |
case-13 | fail→fail | 17,051 | 16,970 | -0% | 1 | 1 | 0% | 2,257 | 7,279 | +223% | 0 | 0 | — |
case-14 | pass→pass | 20,941 | 12,472 | -40% | 1 | 1 | 0% | 2,741 | 7,481 | +173% | 0 | 0 | — |
case-15 | pass→pass | 12,607 | 15,813 | +25% | 1 | 1 | 0% | 2,330 | 6,954 | +198% | 0 | 0 | — |
case-16 | pass→pass | 23,413 | 13,315 | -43% | 1 | 1 | 0% | 1,943 | 6,578 | +239% | 0 | 0 | — |
case-17 | pass→pass | 16,536 | 14,981 | -9% | 1 | 1 | 0% | 2,069 | 6,791 | +228% | 0 | 0 | — |
case-18 | pass→pass | 18,902 | 22,533 | +19% | 1 | 1 | 0% | 2,549 | 8,088 | +217% | 0 | 0 | — |
case-19 | pass→pass | 15,018 | 11,167 | -26% | 1 | 1 | 0% | 2,698 | 7,281 | +170% | 0 | 0 | — |
case-20 | pass→pass | 21,253 | 19,410 | -9% | 1 | 1 | 0% | 3,264 | 8,045 | +146% | 0 | 0 | — |
case-21 | fail→pass | 35,094 | 20,500 | -42% | 1 | 1 | 0% | 3,158 | 8,130 | +157% | 0 | 0 | — |
case-22 | fail→pass | 14,660 | 15,255 | +4% | 1 | 1 | 0% | 2,573 | 6,863 | +167% | 0 | 0 | — |
case-23 | fail→pass | 19,502 | 9,526 | -51% | 1 | 1 | 0% | 2,502 | 6,638 | +165% | 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. 23 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
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.