com um clique
api-designer
// Design and implement REST API endpoints for NutriProfile. Use this skill when creating new endpoints, modifying API schemas, working with Pydantic models, or ensuring OpenAPI compliance. Follows FastAPI best practices.
// Design and implement REST API endpoints for NutriProfile. Use this skill when creating new endpoints, modifying API schemas, working with Pydantic models, or ensuring OpenAPI compliance. Follows FastAPI best practices.
Manage the AI coaching system for NutriProfile. Use this skill when working with personalized nutrition advice, daily tips, motivational messages, challenges, or the coaching features. Handles user context, streak management, and gamification integration.
Manage PostgreSQL database for NutriProfile. Use this skill when working with SQLAlchemy models, Alembic migrations, query optimization, or database schema changes. Covers async operations and Fly Postgres deployment.
Deploy NutriProfile to production. Use this skill when deploying backend to Fly.io, frontend to Cloudflare Pages, managing secrets, running health checks, or troubleshooting deployment issues.
Manage internationalization for NutriProfile's 7 languages. Use this skill when adding translations, creating new i18n keys, checking translation coverage, or working with RTL support for Arabic. Ensures consistency across FR, EN, DE, ES, PT, ZH, AR.
Analyze food items and calculate nutritional values for NutriProfile. Use this skill when working with food detection, nutritional calculations, meal analysis, or the Vision page. Covers calories, proteins, carbs, fats, and portion estimation.
Generate and manage AI-powered recipes for NutriProfile. Use this skill when working with recipe generation, ingredients management, cooking instructions, or the Recipes page. Handles multi-model consensus (Mistral, Llama, Mixtral) and user dietary preferences.
| name | api-designer |
| description | Design and implement REST API endpoints for NutriProfile. Use this skill when creating new endpoints, modifying API schemas, working with Pydantic models, or ensuring OpenAPI compliance. Follows FastAPI best practices. |
| allowed-tools | Read,Write,Edit,Grep,Glob,Bash |
You are a REST API design expert for the NutriProfile application. This skill helps you create well-structured, documented, and secure API endpoints using FastAPI and Pydantic.
https://nutriprofile-api.fly.dev/api/v1http://localhost:8000/api/v1backend/app/api/v1/
├── __init__.py
├── auth.py # Authentication (login, register, refresh)
├── users.py # User management
├── profiles.py # Nutritional profiles
├── vision.py # Food photo analysis
├── recipes.py # Recipe generation
├── tracking.py # Activity & weight tracking
├── dashboard.py # Statistics & achievements
├── coaching.py # AI coaching
├── subscriptions.py # Subscription management
├── webhooks.py # Lemon Squeezy webhooks
└── health.py # Health checks
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.user import User
from app.schemas.recipe import RecipeCreate, RecipeUpdate, RecipeResponse
from app.services.auth import get_current_user
router = APIRouter(prefix="/recipes", tags=["recipes"])
# LIST
@router.get("/", response_model=list[RecipeResponse])
async def list_recipes(
skip: int = 0,
limit: int = 20,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""List all recipes for the current user."""
recipes = await recipe_service.get_user_recipes(db, current_user.id, skip, limit)
return recipes
# CREATE
@router.post("/", response_model=RecipeResponse, status_code=status.HTTP_201_CREATED)
async def create_recipe(
recipe_data: RecipeCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Create a new recipe."""
return await recipe_service.create(db, current_user.id, recipe_data)
# READ
@router.get("/{recipe_id}", response_model=RecipeResponse)
async def get_recipe(
recipe_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get a specific recipe by ID."""
recipe = await recipe_service.get(db, recipe_id, current_user.id)
if not recipe:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Recipe not found"
)
return recipe
# UPDATE
@router.patch("/{recipe_id}", response_model=RecipeResponse)
async def update_recipe(
recipe_id: int,
recipe_data: RecipeUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Update a recipe."""
recipe = await recipe_service.update(db, recipe_id, current_user.id, recipe_data)
if not recipe:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Recipe not found"
)
return recipe
# DELETE
@router.delete("/{recipe_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_recipe(
recipe_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Delete a recipe."""
success = await recipe_service.delete(db, recipe_id, current_user.id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Recipe not found"
)
# backend/app/schemas/recipe.py
from pydantic import BaseModel, Field, validator
from typing import Optional, List
from datetime import datetime
# Base schema (shared fields)
class RecipeBase(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=1000)
prep_time: int = Field(..., ge=0, le=480) # minutes
cook_time: int = Field(..., ge=0, le=480)
servings: int = Field(..., ge=1, le=50)
# Create schema (input for POST)
class RecipeCreate(RecipeBase):
ingredients: List[IngredientCreate]
instructions: List[str] = Field(..., min_items=1)
@validator('instructions')
def validate_instructions(cls, v):
if any(len(i) > 500 for i in v):
raise ValueError('Each instruction must be under 500 characters')
return v
# Update schema (input for PATCH)
class RecipeUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=1000)
prep_time: Optional[int] = Field(None, ge=0, le=480)
cook_time: Optional[int] = Field(None, ge=0, le=480)
servings: Optional[int] = Field(None, ge=1, le=50)
ingredients: Optional[List[IngredientCreate]] = None
instructions: Optional[List[str]] = None
# Response schema (output)
class RecipeResponse(RecipeBase):
id: int
user_id: int
ingredients: List[IngredientResponse]
instructions: List[str]
calories_per_serving: float
protein_per_serving: float
carbs_per_serving: float
fat_per_serving: float
confidence_score: float
created_at: datetime
updated_at: Optional[datetime]
class Config:
from_attributes = True # Pydantic v2
# Nested schema
class IngredientCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
quantity: float = Field(..., gt=0)
unit: str = Field(..., min_length=1, max_length=20)
class IngredientResponse(IngredientCreate):
id: int
calories: float
protein: float
carbs: float
fat: float
# backend/app/services/auth.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
from app.config import settings
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: AsyncSession = Depends(get_db)
) -> User:
"""Validate JWT token and return current user."""
token = credentials.credentials
try:
payload = jwt.decode(
token,
settings.SECRET_KEY,
algorithms=[settings.ALGORITHM]
)
user_id: str = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
user = await db.get(User, int(user_id))
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found"
)
return user
# All endpoints requiring authentication
@router.get("/protected")
async def protected_route(
current_user: User = Depends(get_current_user)
):
return {"user_id": current_user.id}
# Optional authentication
async def get_current_user_optional(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
db: AsyncSession = Depends(get_db)
) -> Optional[User]:
if not credentials:
return None
# ... validate token
from app.services.subscription import SubscriptionService
@router.post("/vision/analyze")
async def analyze_food(
image_data: ImageUpload,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Analyze food photo with tier limits."""
subscription_service = SubscriptionService(db)
# Check usage limit
can_use = await subscription_service.check_limit(
current_user.id,
"vision_analyses"
)
if not can_use:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Daily analysis limit reached. Upgrade to Premium for unlimited analyses."
)
# Proceed with analysis
result = await vision_agent.analyze(image_data)
# Track usage
await subscription_service.increment_usage(
current_user.id,
"vision_analyses"
)
return result
from fastapi import HTTPException, status
# 400 Bad Request - Invalid input
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid email format"
)
# 401 Unauthorized - Not authenticated
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"
)
# 403 Forbidden - Not authorized
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to access this resource"
)
# 404 Not Found - Resource doesn't exist
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Recipe not found"
)
# 422 Unprocessable Entity - Validation error (automatic with Pydantic)
# 429 Too Many Requests - Rate limit
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded. Try again later."
)
# backend/app/main.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
return JSONResponse(
status_code=400,
content={"detail": str(exc)}
)
{
"id": 1,
"name": "Bowl Méditerranéen",
"calories_per_serving": 450,
"created_at": "2026-01-15T10:00:00Z"
}
{
"items": [...],
"total": 100,
"page": 1,
"per_page": 20,
"pages": 5
}
{
"detail": "Recipe not found"
}
{
"detail": [
{
"loc": ["body", "name"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
# backend/app/main.py
app = FastAPI(
title="NutriProfile API",
description="API for nutritional profiling and AI-powered food analysis",
version="1.0.0",
openapi_tags=[
{"name": "auth", "description": "Authentication operations"},
{"name": "users", "description": "User management"},
{"name": "profiles", "description": "Nutritional profiles"},
{"name": "vision", "description": "Food photo analysis"},
{"name": "recipes", "description": "AI recipe generation"},
{"name": "tracking", "description": "Activity and weight tracking"},
{"name": "coaching", "description": "AI coaching"},
{"name": "subscriptions", "description": "Subscription management"},
]
)
@router.post(
"/analyze",
response_model=AnalysisResponse,
summary="Analyze food photo",
description="""
Analyze a food photo using AI vision models.
**Requires authentication.**
**Tier limits:**
- Free: 3 analyses/day
- Premium: Unlimited
- Pro: Unlimited
Returns detected food items with nutritional estimates.
""",
responses={
200: {"description": "Analysis completed successfully"},
400: {"description": "Invalid image data"},
401: {"description": "Not authenticated"},
403: {"description": "Daily limit reached"},
}
)
async def analyze_food(...):
...
# tests/test_recipes.py
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_create_recipe(async_client: AsyncClient, auth_headers: dict):
response = await async_client.post(
"/api/v1/recipes",
headers=auth_headers,
json={
"name": "Test Recipe",
"prep_time": 15,
"cook_time": 30,
"servings": 4,
"ingredients": [
{"name": "poulet", "quantity": 500, "unit": "g"}
],
"instructions": ["Step 1", "Step 2"]
}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Test Recipe"
assert "id" in data