Skip to main content

Transform services in default endpoints integrated by FastAPI and Pydantic.

Project description

fast-serve-api

Transform your Python functions into production-ready REST APIs with minimal code. FastServeApi automatically generates FastAPI endpoints from static methods, handling request/response models, validation, and error handling for you.

Key Benefits

  • Zero Boilerplate: Convert static methods to REST endpoints with a single inheritance
  • Type Safety: Automatic validation using Python type hints and Pydantic
  • Flexible Input: Accept both wrapped JSON objects and raw values for single-parameter endpoints
  • Standardized Responses: Built-in response models for consistency
  • Error Handling: Automatic exception catching with detailed error responses
  • FastAPI Compatible: Seamlessly integrates with existing FastAPI applications

Installation

pip install fast-serve-api

Or using uv:

uv add fast-serve-api

Quick Start

from fastapi import FastAPI
from fast_serve_api import FastServeApi

class CalculatorService(FastServeApi):
    @staticmethod
    def add(a: int, b: int) -> int:
        return a + b
    
    @staticmethod
    def multiply(a: int, b: int) -> int:
        return a * b

app = FastAPI()
CalculatorService.initialize(app)

# That's it! You now have:
# POST /calculator/add
# POST /calculator/multiply

How It Works

FastServeApi automatically:

  1. Converts your class name to snake_case for the URL path
  2. Creates POST endpoints for each static method
  3. Generates Pydantic models from method signatures
  4. Handles request parsing and response formatting
  5. Catches exceptions and returns standardized error responses

Architectural Decisions

POST-Only Endpoints

All endpoints are POST methods by design. This ensures:

  • Consistent parameter handling across all endpoints
  • Support for complex request bodies
  • No URL length limitations
  • Uniform API interface

JSON Format

  • Input: All endpoints receive JSON payloads
  • Output: All endpoints return JSON responses
  • Content-Type: Always application/json

Response Models and Reserved Fields

To guarantee consistent response formats, all Pydantic model responses must inherit from our base models, which include reserved fields:

FastServeApiModel (single object responses):

  • success (bool): Indicates if the operation succeeded
  • message (str): Human-readable response message
  • status_code (int | None): Custom status code (see below)

FastServeApiListModel (list responses) includes all above plus:

  • page (int): Current page number
  • size (int): Page size
  • total (int | None): Total number of items
  • last_page (bool): Whether this is the last page

FastServeApiErrorModel (error responses):

  • success (bool): Always false for errors
  • message (str): Error description
  • stack_trace (str): Full exception traceback
  • status_code (int): HTTP status code

Status Code Philosophy

Only 3 status codes are automatically set by the framework:

  • 200 OK: Successful request
  • 422 Unprocessable Entity: Invalid request parameters
  • 500 Internal Server Error: Unhandled exceptions

The status_code field in response models is independent of the HTTP status code. This allows you to:

  • Define application-specific status codes
  • Provide detailed error codes while maintaining HTTP 200
  • Include both success and error codes in responses

Example:

class UserResponse(FastServeApiModel):
    user_id: int
    username: str
    
# Can return with custom status_code
return UserResponse(
    success=True,
    message="User created but email verification pending",
    status_code=1001,  # Custom app code, HTTP will still be 200
    user_id=123,
    username="john"
)

Examples

Basic Usage

from typing import List, Dict, Optional
from fast_serve_api import FastServeApi

class UserService(FastServeApi):
    @staticmethod
    def get_user(user_id: int) -> Dict[str, any]:
        return {"id": user_id, "name": "John Doe"}
    
    @staticmethod
    def create_user(name: str, email: str, age: Optional[int] = None) -> Dict[str, any]:
        return {"name": name, "email": email, "age": age}
    
    @staticmethod
    def list_users(limit: int = 10, offset: int = 0) -> List[Dict[str, any]]:
        return [{"id": i, "name": f"User {i}"} for i in range(offset, offset + limit)]

Using Pydantic Models

from pydantic import BaseModel
from fast_serve_api import FastServeApi, FastServeApiModel

class UserInput(BaseModel):
    name: str
    email: str
    age: Optional[int] = None

class UserResponse(FastServeApiModel):
    user_id: int
    username: str

class UserService(FastServeApi):
    @staticmethod
    def create_user(user: UserInput) -> UserResponse:
        # FastServeApiModel requires success and message fields
        return UserResponse(
            success=True,
            message="User created successfully",
            user_id=123,
            username=user.name
        )

Single Parameter Flexibility

For endpoints with a single parameter, fast-serve-api accepts both wrapped and raw values:

class GreetingService(FastServeApi):
    @staticmethod
    def say_hello(name: str) -> str:
        return f"Hello, {name}!"

# Both request formats work:
# {"name": "Alice"}  "Hello, Alice!"
# "Alice"            "Hello, Alice!"

List Responses with Pagination

from fast_serve_api import FastServeApiListModel

class ProductListResponse(FastServeApiListModel):
    products: List[Dict[str, any]]

class ProductService(FastServeApi):
    @staticmethod
    def list_products(page: int = 1, size: int = 20) -> ProductListResponse:
        products = [{"id": i, "name": f"Product {i}"} for i in range(size)]
        return ProductListResponse(
            success=True,
            message="Products retrieved",
            page=page,
            size=size,
            last_page=page >= 5,
            total=100,
            products=products
        )

Complex Types and Nested Models

from typing import Set, Tuple, Literal

class DataService(FastServeApi):
    @staticmethod
    def process_tags(tags: Set[str]) -> List[str]:
        # Sets are automatically converted from JSON arrays
        return sorted(tags)
    
    @staticmethod
    def get_coordinates(location: str) -> Tuple[float, float]:
        # Return tuples for fixed-size data
        return (40.7128, -74.0060)
    
    @staticmethod
    def set_status(status: Literal["active", "inactive", "pending"]) -> str:
        # Literal types for constrained values
        return f"Status set to: {status}"

Error Handling

Exceptions are automatically caught and returned as standardized error responses:

class SafeService(FastServeApi):
    @staticmethod
    def divide(a: float, b: float) -> float:
        if b == 0:
            raise ValueError("Division by zero is not allowed")
        return a / b

# POST /safe/divide {"a": 10, "b": 0}
# HTTP Status: 500
# Response: {
#   "success": false,
#   "message": "Division by zero is not allowed",
#   "stack_trace": "...",
#   "status_code": 500
# }

Note: The HTTP status code is automatically set to 500 for unhandled exceptions. The status_code field in the response body mirrors this for consistency.

Custom Status Codes

Use the status_code field to communicate application-specific statuses:

class OrderService(FastServeApi):
    @staticmethod
    def process_order(order_id: int) -> OrderResponse:
        order = get_order(order_id)
        
        if order.status == "pending_payment":
            return OrderResponse(
                success=True,
                message="Order awaiting payment",
                status_code=201,  # Custom: Payment pending
                order_id=order_id,
                status="pending_payment"
            )
        elif order.status == "backordered":
            return OrderResponse(
                success=True,
                message="Order processed but items backordered",
                status_code=202,  # Custom: Backordered
                order_id=order_id,
                status="backordered"
            )
        else:
            return OrderResponse(
                success=True,
                message="Order processed successfully",
                status_code=199,  # Custom: Success
                order_id=order_id,
                status="completed"
            )

# All above responses return HTTP 200, but with different status_code values

Advanced Features

Type Validation

fast-serve-api validates return types at runtime:

class TypedService(FastServeApi):
    @staticmethod
    def get_numbers(count: int) -> List[int]:
        return list(range(count))  # Valid
        # return ["a", "b", "c"]   # Would raise TypeError

    @staticmethod
    def get_user_data() -> Dict[str, str]:
        return {"name": "John", "email": "john@example.com"}  # Valid
        # return {"name": "John", "age": 30}  # Would raise TypeError (age is int)

Optional Parameters

All-optional endpoints can be called without a request body:

class SearchService(FastServeApi):
    @staticmethod
    def search(query: str = "", limit: int = 10, offset: int = 0) -> List[str]:
        return [f"Result {i}" for i in range(offset, offset + limit)]

# POST /search/search (no body) Uses all defaults
# POST /search/search {"query": "test"} Uses provided query, other defaults

Endpoint Naming Convention

Class names are converted from CamelCase to snake_case:

  • UserService - /user/...
  • ProductAPIService - /product_api/...
  • MultiWordServiceName - /multi_word_service_name/...

The "Service" suffix is automatically removed if present.

API Reference

Response Models

All Pydantic model responses must inherit from:

  • FastServeApiModel: For single object responses

    class MyResponse(FastServeApiModel):
        success: bool  # Required
        message: str   # Required
        status_code: Optional[int] = None
        # ... your fields
    
  • FastServeApiListModel: For list responses with pagination

    class MyListResponse(FastServeApiListModel):
        success: bool  # Required
        message: str   # Required
        page: int      # Required
        size: int      # Required
        last_page: bool # Required
        total: Optional[int] = None
        # ... your list field
    

Reserved Field Names

The following field names are reserved and used by the framework:

  • success - Operation success indicator
  • message - Response message
  • status_code - Custom application status code
  • stack_trace - Error stack trace (error responses only)
  • page - Current page number (list responses)
  • size - Page size (list responses)
  • total - Total items count (list responses)
  • last_page - Last page indicator (list responses)

Avoid using these names for your custom fields to prevent conflicts.

Supported Types

  • Primitives: str, int, float, bool, None
  • Collections: List[T], Dict[K, V], Set[T], Tuple[T, ...]
  • Optional: Optional[T], Union[T, None]
  • Literal: Literal["option1", "option2", ...]
  • Pydantic Models: Any BaseModel subclass

Comparison: Traditional vs fast-serve-api

Traditional FastAPI Approach

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class AddRequest(BaseModel):
    a: int
    b: int

class MultiplyRequest(BaseModel):
    x: float
    y: float

class DivideRequest(BaseModel):
    dividend: float
    divisor: float

class CalculatorResponse(BaseModel):
    result: float
    operation: str

@app.post("/calculator/add", response_model=CalculatorResponse)
async def add_numbers(request: AddRequest):
    try:
        result = request.a + request.b
        return CalculatorResponse(result=result, operation="addition")
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/calculator/multiply", response_model=CalculatorResponse)
async def multiply_numbers(request: MultiplyRequest):
    try:
        result = request.x * request.y
        return CalculatorResponse(result=result, operation="multiplication")
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/calculator/divide", response_model=CalculatorResponse)
async def divide_numbers(request: DivideRequest):
    try:
        if request.divisor == 0:
            raise ValueError("Cannot divide by zero")
        result = request.dividend / request.divisor
        return CalculatorResponse(result=result, operation="division")
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

With fast-serve-api

from fast_serve_api import FastServeApi

class CalculatorService(FastServeApi):
    @staticmethod
    def add(a: int, b: int) -> float:
        return a + b
    
    @staticmethod
    def multiply(x: float, y: float) -> float:
        return x * y
    
    @staticmethod
    def divide(dividend: float, divisor: float) -> float:
        if divisor == 0:
            raise ValueError("Cannot divide by zero")
        return dividend / divisor

app = FastAPI()
CalculatorService.initialize(app)

Result: less code, automatic error handling, and consistent API structure!

Contributing

We welcome contributions! Here's how to set up your development environment and contribute to fast-serve-api.

Development Setup

  1. Clone the repository

    git clone https://github.com/ygorhora/fast-serve-api.git
    cd fast-serve-api
    
  2. Install uv (recommended package manager)

    # On macOS/Linux
    curl -LsSf https://astral.sh/uv/install.sh | sh
    
    # On Windows
    powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
    
  3. Create virtual environment and install dependencies

    # Install all dependencies including dev/test dependencies
    uv sync --extra test
    
  4. Run tests

    # Run all tests
    uv run pytest
    
    # Run with coverage
    uv run pytest --cov=src/fast_serve_api --cov-report=term-missing
    
    # Run specific test file
    uv run pytest tests/test_basic_endpoints.py
    
    # Run with verbose output
    uv run pytest -v
    
  5. Run the example server (if you create a test file)

    # Create a test.py file with your service
    uvicorn test:app --reload
    

Project Structure

fast-serve-api/
├── src/
│   └── fast_serve_api/
│       ├── __init__.py
│       ├── fast_serve_api.py      # Main class implementation
│       └── models/                # Response model classes
│           ├── fast_serve_api_model.py
│           ├── fast_serve_api_list_model.py
│           └── fast_serve_api_error_model.py
├── tests/                         # Test suite
│   ├── conftest.py               # Pytest fixtures
│   ├── models.py                 # Test models
│   ├── services.py               # Test services
│   └── test_*.py                 # Test files
├── pyproject.toml                # Project configuration
└── README.md                     # This file

Development Guidelines

  1. Code Style

    • Code is automatically formatted with Ruff
    • Follow PEP 8 conventions (enforced by Ruff)
    • Use type hints for all function parameters and returns
    • Keep methods focused and single-purpose
  2. Testing

    • Write tests for any new functionality
    • Ensure all tests pass before submitting PR
    • Aim for high test coverage (currently at 98%)
  3. Adding New Features

    • Check existing issues/PRs to avoid duplicates
    • Discuss major changes in an issue first
    • Update documentation and tests
  4. Commit Messages

    • Use clear, descriptive commit messages
    • Follow conventional commits format (optional but appreciated)
    • Example: feat: add support for async methods

Running Common Development Tasks

# Install new dependency
uv add <package-name>

# Install dev dependency
uv add --dev <package-name>

# Clean up
rm -rf dist build *.egg-info

Submitting a Pull Request

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run linting and formatting:
    uv run ruff check --fix .
    uv run ruff format .
    
  5. Run tests to ensure everything works:
    uv run pytest
    
  6. Commit your changes
  7. Push to your fork (git push origin feature/amazing-feature)
  8. Open a Pull Request with a clear description

Need Help?

  • Check the issues page
  • Read through the test files for usage examples
  • Feel free to ask questions in issues or discussions

License

This project is licensed under the MIT License - see the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fast_serve_api-1.0.0.tar.gz (50.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fast_serve_api-1.0.0-py3-none-any.whl (12.0 kB view details)

Uploaded Python 3

File details

Details for the file fast_serve_api-1.0.0.tar.gz.

File metadata

  • Download URL: fast_serve_api-1.0.0.tar.gz
  • Upload date:
  • Size: 50.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.3

File hashes

Hashes for fast_serve_api-1.0.0.tar.gz
Algorithm Hash digest
SHA256 b76322d3dab7e6a5685823ace7f72849a1f3d88fc719f936c909a14f00af55cf
MD5 0f1c22798fdebb9a3ec31bc8a32f09df
BLAKE2b-256 c698c5fb77e659a4bd7bc47f12cd590962559f458659e0a4c8e24806e3bd0c8e

See more details on using hashes here.

File details

Details for the file fast_serve_api-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fast_serve_api-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cddda6dd7d962ff0811410ca6ebf84504b9748c4c39e080bd2b984e00af77920
MD5 fdc721c1f08fec91f474297ec996f488
BLAKE2b-256 1b77284dedc00fc4d8e41c170285f88c8811640d18a8a044bbbdfbe87630f3ee

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page