Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

14 KiB

FastAPI Endpoints Generator

Create comprehensive FastAPI endpoints with proper structure, validation, and documentation.

Purpose

This command helps you quickly create FastAPI endpoints with Pydantic models, dependency injection, and automatic API documentation.

Usage

/api-endpoints

What this command does

  1. Creates API endpoints with proper HTTP methods
  2. Adds Pydantic models for request/response validation
  3. Implements dependency injection for database and auth
  4. Includes error handling and status codes
  5. Generates automatic documentation with OpenAPI

Example Output

# main.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
import uvicorn

from app.database import get_db, engine
from app.models import models
from app.routers import auth, users, posts, comments
from app.core.config import settings

# Create database tables
models.Base.metadata.create_all(bind=engine)

# Initialize FastAPI app
app = FastAPI(
    title="Blog API",
    description="A comprehensive blog API built with FastAPI",
    version="1.0.0",
    docs_url="/docs",
    redoc_url="/redoc"
)

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.ALLOWED_HOSTS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Include routers
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
app.include_router(users.router, prefix="/users", tags=["Users"])
app.include_router(posts.router, prefix="/posts", tags=["Posts"])
app.include_router(comments.router, prefix="/comments", tags=["Comments"])

@app.get("/", tags=["Root"])
async def root():
    """API root endpoint."""
    return {
        "message": "Welcome to Blog API",
        "version": "1.0.0",
        "docs": "/docs",
        "redoc": "/redoc"
    }

@app.get("/health", tags=["Health"])
async def health_check():
    """Health check endpoint."""
    return {"status": "healthy", "timestamp": "2024-01-01T00:00:00Z"}

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        reload=True
    )
# app/routers/posts.py
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session

from app.database import get_db
from app.schemas import post_schemas
from app.services import post_service
from app.core.dependencies import get_current_user, get_current_active_user
from app.models.user import User

router = APIRouter()

@router.get("/", response_model=List[post_schemas.PostResponse])
async def get_posts(
    skip: int = Query(0, ge=0, description="Number of posts to skip"),
    limit: int = Query(10, ge=1, le=100, description="Number of posts to return"),
    search: Optional[str] = Query(None, description="Search in title and content"),
    category: Optional[str] = Query(None, description="Filter by category"),
    published: Optional[bool] = Query(True, description="Filter by published status"),
    db: Session = Depends(get_db)
):
    """
    Get all posts with pagination and filtering.
    
    - **skip**: Number of posts to skip (for pagination)
    - **limit**: Maximum number of posts to return (1-100)
    - **search**: Search term for title and content
    - **category**: Filter posts by category
    - **published**: Filter by published status
    """
    posts = post_service.get_posts(
        db=db,
        skip=skip,
        limit=limit,
        search=search,
        category=category,
        published=published
    )
    return posts

@router.get("/{post_id}", response_model=post_schemas.PostResponse)
async def get_post(
    post_id: int,
    db: Session = Depends(get_db)
):
    """
    Get a specific post by ID.
    
    - **post_id**: Unique identifier for the post
    """
    post = post_service.get_post(db=db, post_id=post_id)
    if not post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Post not found"
        )
    return post

@router.post("/", response_model=post_schemas.PostResponse, status_code=status.HTTP_201_CREATED)
async def create_post(
    post: post_schemas.PostCreate,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_active_user)
):
    """
    Create a new post.
    
    - **title**: Post title (required)
    - **content**: Post content (required)
    - **category**: Post category (optional)
    - **published**: Publication status (default: false)
    """
    return post_service.create_post(
        db=db,
        post=post,
        user_id=current_user.id
    )

@router.put("/{post_id}", response_model=post_schemas.PostResponse)
async def update_post(
    post_id: int,
    post_update: post_schemas.PostUpdate,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_active_user)
):
    """
    Update an existing post.
    
    - **post_id**: Unique identifier for the post
    - **title**: Updated post title (optional)
    - **content**: Updated post content (optional)
    - **category**: Updated post category (optional)
    - **published**: Updated publication status (optional)
    """
    post = post_service.get_post(db=db, post_id=post_id)
    if not post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Post not found"
        )
    
    if post.author_id != current_user.id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Not authorized to update this post"
        )
    
    return post_service.update_post(
        db=db,
        post_id=post_id,
        post_update=post_update
    )

@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_post(
    post_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_active_user)
):
    """
    Delete a post.
    
    - **post_id**: Unique identifier for the post to delete
    """
    post = post_service.get_post(db=db, post_id=post_id)
    if not post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Post not found"
        )
    
    if post.author_id != current_user.id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Not authorized to delete this post"
        )
    
    post_service.delete_post(db=db, post_id=post_id)

@router.post("/{post_id}/like", response_model=post_schemas.PostResponse)
async def like_post(
    post_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_active_user)
):
    """
    Like/unlike a post.
    
    - **post_id**: Unique identifier for the post to like
    """
    post = post_service.get_post(db=db, post_id=post_id)
    if not post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Post not found"
        )
    
    return post_service.toggle_like(
        db=db,
        post_id=post_id,
        user_id=current_user.id
    )

@router.get("/{post_id}/comments", response_model=List[post_schemas.CommentResponse])
async def get_post_comments(
    post_id: int,
    skip: int = Query(0, ge=0),
    limit: int = Query(10, ge=1, le=100),
    db: Session = Depends(get_db)
):
    """
    Get all comments for a specific post.
    
    - **post_id**: Unique identifier for the post
    - **skip**: Number of comments to skip
    - **limit**: Maximum number of comments to return
    """
    post = post_service.get_post(db=db, post_id=post_id)
    if not post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Post not found"
        )
    
    return post_service.get_post_comments(
        db=db,
        post_id=post_id,
        skip=skip,
        limit=limit
    )
# app/schemas/post_schemas.py
from typing import Optional, List
from datetime import datetime
from pydantic import BaseModel, Field, validator

class PostBase(BaseModel):
    """Base post schema."""
    title: str = Field(..., min_length=1, max_length=200, description="Post title")
    content: str = Field(..., min_length=1, description="Post content")
    category: Optional[str] = Field(None, max_length=50, description="Post category")
    published: bool = Field(False, description="Publication status")

class PostCreate(PostBase):
    """Schema for creating a post."""
    
    @validator('title')
    def validate_title(cls, v):
        if not v.strip():
            raise ValueError('Title cannot be empty')
        return v.strip()
    
    @validator('content')
    def validate_content(cls, v):
        if len(v.strip()) < 10:
            raise ValueError('Content must be at least 10 characters long')
        return v.strip()

class PostUpdate(BaseModel):
    """Schema for updating a post."""
    title: Optional[str] = Field(None, min_length=1, max_length=200)
    content: Optional[str] = Field(None, min_length=1)
    category: Optional[str] = Field(None, max_length=50)
    published: Optional[bool] = None
    
    @validator('title')
    def validate_title(cls, v):
        if v is not None and not v.strip():
            raise ValueError('Title cannot be empty')
        return v.strip() if v else v
    
    @validator('content')
    def validate_content(cls, v):
        if v is not None and len(v.strip()) < 10:
            raise ValueError('Content must be at least 10 characters long')
        return v.strip() if v else v

class PostResponse(PostBase):
    """Schema for post responses."""
    id: int
    author_id: int
    created_at: datetime
    updated_at: datetime
    like_count: int = 0
    comment_count: int = 0
    
    class Config:
        from_attributes = True

class CommentBase(BaseModel):
    """Base comment schema."""
    content: str = Field(..., min_length=1, max_length=1000, description="Comment content")

class CommentCreate(CommentBase):
    """Schema for creating a comment."""
    post_id: int = Field(..., description="ID of the post to comment on")

class CommentResponse(CommentBase):
    """Schema for comment responses."""
    id: int
    post_id: int
    author_id: int
    created_at: datetime
    updated_at: datetime
    
    class Config:
        from_attributes = True
# app/services/post_service.py
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import or_

from app.models.post import Post, Comment, PostLike
from app.schemas.post_schemas import PostCreate, PostUpdate

def get_posts(
    db: Session,
    skip: int = 0,
    limit: int = 10,
    search: Optional[str] = None,
    category: Optional[str] = None,
    published: Optional[bool] = True
) -> List[Post]:
    """Get posts with filtering and pagination."""
    query = db.query(Post)
    
    if published is not None:
        query = query.filter(Post.published == published)
    
    if category:
        query = query.filter(Post.category == category)
    
    if search:
        query = query.filter(
            or_(
                Post.title.contains(search),
                Post.content.contains(search)
            )
        )
    
    return query.offset(skip).limit(limit).all()

def get_post(db: Session, post_id: int) -> Optional[Post]:
    """Get a single post by ID."""
    return db.query(Post).filter(Post.id == post_id).first()

def create_post(db: Session, post: PostCreate, user_id: int) -> Post:
    """Create a new post."""
    db_post = Post(**post.dict(), author_id=user_id)
    db.add(db_post)
    db.commit()
    db.refresh(db_post)
    return db_post

def update_post(
    db: Session,
    post_id: int,
    post_update: PostUpdate
) -> Optional[Post]:
    """Update an existing post."""
    db_post = db.query(Post).filter(Post.id == post_id).first()
    if not db_post:
        return None
    
    update_data = post_update.dict(exclude_unset=True)
    for field, value in update_data.items():
        setattr(db_post, field, value)
    
    db.commit()
    db.refresh(db_post)
    return db_post

def delete_post(db: Session, post_id: int) -> bool:
    """Delete a post."""
    db_post = db.query(Post).filter(Post.id == post_id).first()
    if not db_post:
        return False
    
    db.delete(db_post)
    db.commit()
    return True

def toggle_like(db: Session, post_id: int, user_id: int) -> Post:
    """Toggle like status for a post."""
    existing_like = db.query(PostLike).filter(
        PostLike.post_id == post_id,
        PostLike.user_id == user_id
    ).first()
    
    if existing_like:
        db.delete(existing_like)
    else:
        new_like = PostLike(post_id=post_id, user_id=user_id)
        db.add(new_like)
    
    db.commit()
    return get_post(db, post_id)

Features Included

API Documentation

  • Automatic OpenAPI schema generation
  • Interactive docs at /docs
  • ReDoc documentation at /redoc
  • Request/Response examples in schemas

Validation & Serialization

  • Pydantic models for data validation
  • Custom validators for business rules
  • Type hints for better IDE support
  • Automatic data conversion and validation

Error Handling

  • HTTP status codes for different scenarios
  • Detailed error messages with context
  • Input validation errors with field-specific messages
  • Custom exception handlers for consistent responses

Security

  • JWT authentication with dependencies
  • Role-based access control for endpoints
  • CORS middleware for cross-origin requests
  • Input sanitization through Pydantic

Performance

  • Database query optimization with SQLAlchemy
  • Pagination support for large datasets
  • Async/await support for concurrent requests
  • Connection pooling for database efficiency

Testing Example

# tests/test_posts.py
import pytest
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_get_posts():
    """Test getting posts."""
    response = client.get("/posts/")
    assert response.status_code == 200
    assert isinstance(response.json(), list)

def test_create_post():
    """Test creating a new post."""
    post_data = {
        "title": "Test Post",
        "content": "This is a test post content.",
        "published": True
    }
    response = client.post("/posts/", json=post_data)
    assert response.status_code == 201
    assert response.json()["title"] == "Test Post"