Project Setup
Project Setup¶
This chapter introduces how to correctly set up and configure an SRF project, including project structure, configuration management, database setup, etc.
Recommended Project Structure¶
Assume an SRF project has the following structure:
myproject/
├── app.py # Application entry point
├── config.py # Configuration file
├── requirements.txt # Dependency list
├── .env # Environment variables
├── models/ # Data models
│ ├── __init__.py
│ ├── product.py
│ ├── order.py
│ └── category.py
├── schemas/ # Pydantic Schemas
│ ├── __init__.py
│ ├── product.py
│ ├── order.py
│ └── category.py
├── viewsets/ # ViewSets
│ ├── __init__.py
│ ├── product.py
│ ├── order.py
│ └── category.py
├── routes.py # Route configuration
├── permissions.py # Custom permission classes
├── filters.py # Custom filters
├── middleware.py # Custom middlewares
├── utils/ # Utility functions
│ ├── __init__.py
│ ├── helpers.py
│ └── validators.py
└── tests/ # Tests
├── __init__.py
├── test_products.py
└── test_orders.py
Configuration Management¶
Environment Variables¶
Create a .env file to store sensitive information:
# .env
SECRET_KEY=your-secret-key-here
DATABASE_URL=sqlite://db.sqlite3
REDIS_URL=redis://localhost:6379/0
# JWT configuration
JWT_SECRET=your-jwt-secret
JWT_ACCESS_TOKEN_EXPIRES=86400
# Email configuration (names read by srf.tools.email)
FROM_EMAIL=your-email@gmail.com
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
PASSWORD=your-password
# GitHub OAuth
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
Configuration File¶
Create config.py to manage configurations:
import os
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
class Config:
"""Base configuration"""
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key")
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite://db.sqlite3")
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
# JWT configuration
JWT_SECRET = os.getenv("JWT_SECRET", SECRET_KEY)
# JWT_ACCESS_TOKEN_EXPIRES is defined in srf.config.settings; not passed to sanic-jwt
# Email: send_email reads srf.config.settings.EmailConfig (FROM_EMAIL/SMTP_SERVER/SMTP_PORT/PASSWORD environment variables), not this Config class attributes
# Social login (uppercase keys, consistent with SOCIAL_CONFIG)
SOCIAL_CONFIG = {
"github": {
"CLIENT_ID": os.getenv("GITHUB_CLIENT_ID"),
"CLIENT_SECRET": os.getenv("GITHUB_CLIENT_SECRET"),
"REDIRECT_URI": os.getenv("GITHUB_REDIRECT_URI"),
...
}
}
# CORS configuration
CORS_ORIGINS = ["http://localhost:3000", "http://localhost:8080"]
# Rate limiting configuration
RATE_LIMIT_ENABLED = True
RATE_LIMIT_PER_MINUTE = 60
class DevelopmentConfig(Config):
"""Development environment configuration"""
DEBUG = True
DATABASE_URL = "sqlite://db_dev.sqlite3"
class ProductionConfig(Config):
"""Production environment configuration"""
DEBUG = False
DATABASE_URL = os.getenv("DATABASE_URL")
class TestConfig(Config):
"""Test environment configuration"""
TESTING = True
DATABASE_URL = "sqlite://:memory:"
# Choose configuration based on environment variable
config_map = {
"development": DevelopmentConfig,
"production": ProductionConfig,
"test": TestConfig,
}
env = os.getenv("ENVIRONMENT", "development")
config = config_map[env]
Application Configuration¶
Apply configuration in app.py:
from sanic import Sanic
from tortoise.contrib.sanic import register_tortoise
from srf.config import settings
from config import config
app = Sanic("MyApp")
# Apply configuration to Sanic
app.config.update_config(config)
# Configure SRF
settings.set_app(app)
# Configure database
register_tortoise(
app,
db_url=config.DATABASE_URL,
modules={"models": ["models"]},
generate_schemas=True,
)
Database Setup¶
Tortoise ORM Configuration¶
SRF uses Tortoise ORM as the default ORM framework.
Basic Configuration¶
register_tortoise(
app,
db_url="sqlite://db.sqlite3",
modules={"models": ["models"]},
generate_schemas=True,
)
PostgreSQL Configuration¶
register_tortoise(
app,
db_url="postgres://user:password@localhost:5432/dbname",
modules={"models": ["models"]},
generate_schemas=True,
)
MySQL Configuration¶
register_tortoise(
app,
db_url="mysql://user:password@localhost:3306/dbname",
modules={"models": ["models"]},
generate_schemas=True,
)
Advanced Configuration¶
from tortoise.contrib.sanic import register_tortoise
register_tortoise(
app,
config={
"connections": {
"default": {
"engine": "tortoise.backends.asyncpg",
"credentials": {
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "password",
"database": "mydb",
"minsize": 1,
"maxsize": 5,
}
}
},
"apps": {
"models": {
"models": ["models", "aerich.models"],
"default_connection": "default",
}
},
"use_tz": False,
"timezone": "Asia/Shanghai"
},
generate_schemas=True,
)
Database Migrations¶
Use Aerich for database migrations:
Install Aerich¶
Initialize Aerich¶
Initialize Database¶
Create Migration¶
Apply Migration¶
Rollback Migration¶
Configuration File Example¶
Create config.py for Aerich:
TORTOISE_ORM = {
"connections": {
"default": "sqlite://db.sqlite3"
},
"apps": {
"models": {
"models": ["models", "aerich.models"],
"default_connection": "default",
}
},
}
Authentication Setup¶
JWT and Route Registration (Recommended)¶
Use register_auth_urls: it internally calls setup_auth and registers routes for login, registration, email verification, social login, etc. Do not call setup_auth before register_auth_urls (it will be initialized twice).
from srf.auth.route import register_auth_urls
# Initialize JWT and register authentication routes (internal call to setup_auth)
register_auth_urls(app, prefix="/api/auth")
Only call setup_auth directly when you need to customize sanic-jwt parameters and are not using register_auth_urls:
from srf.auth.viewset import setup_auth
jwt = setup_auth(app, url_prefix="/api/auth", secret=config.JWT_SECRET)
app.ctx.jwt = jwt
Social Login Configuration¶
Configure social login in the configuration file:
class Config:
SOCIAL_CONFIG = {
"github": {
"CLIENT_ID": os.getenv("GITHUB_CLIENT_ID"),
"CLIENT_SECRET": os.getenv("GITHUB_CLIENT_SECRET"),
"REDIRECT_URI": os.getenv(
"GITHUB_REDIRECT_URI",
"http://localhost:8000/api/auth/social/callback",
),
...
}
}
Public Endpoint Configuration¶
Configure endpoints that do not require authentication:
class Config:
NON_AUTH_ENDPOINTS = [
"/api/auth/login",
"/api/auth/register",
"/api/auth/send-verification-email",
"/api/products", # Public product list
"/health/", # Health check
...
]
Middleware Setup¶
Authentication Middleware¶
from srf.middleware.authmiddleware import set_user_to_request_ctx
@app.middleware("request")
async def auth_middleware(request):
"""Authentication middleware"""
await set_user_to_request_ctx(request)
Rate Limiting Middleware¶
from srf.middleware.throttlemiddleware import (
MemoryStorage,
IPRateLimit,
UserRateLimit,
throttle_rate
)
# Create storage
storage = MemoryStorage()
# Configure rate limiting rules
app.config.REQUEST_LIMITERS = [
IPRateLimit(100, 60, storage), # IP: 100 requests per minute
UserRateLimit(1000, 60, storage), # User: 1000 requests per minute
]
@app.middleware("request")
async def throttle_middleware(request):
"""Rate limiting middleware"""
if not await throttle_rate(request):
from sanic.response import json
return json({"error": "Too many requests, please try again later"}, status=429)
CORS Middleware¶
from sanic_cors import CORS
# Configure CORS
CORS(
app,
origins=config.CORS_ORIGINS,
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type"],
expose_headers=["Content-Range", "X-Content-Range"],
supports_credentials=True,
)
Route Setup¶
Basic Route Registration¶
Create routes.py:
from srf.route import SanicRouter
from viewsets.product import ProductViewSet
from viewsets.order import OrderViewSet
from viewsets.category import CategoryViewSet
def register_routes(app):
"""Register all routes"""
router = SanicRouter(prefix="api")
# Register ViewSets
router.register("products", ProductViewSet, name="products")
router.register("orders", OrderViewSet, name="orders")
router.register("categories", CategoryViewSet, name="categories")
# Add routes to the application
app.blueprint(router.get_blueprint())
Call in app.py:
from routes import register_routes
app = Sanic("MyApp")
# ... other configurations ...
# Register routes
register_routes(app)
Multi-Version API¶
from srf.route import SanicRouter
def register_routes(app):
# v1 API
router_v1 = SanicRouter(prefix="api/v1")
router_v1.register("products", ProductViewSetV1)
app.blueprint(router_v1.get_blueprint())
# v2 API
router_v2 = SanicRouter(prefix="api/v2")
router_v2.register("products", ProductViewSetV2)
app.blueprint(router_v2.get_blueprint())
Health Check Setup¶
from srf.health.route import bp as health_bp
from srf.health.checks import RedisCheck
# Register health check routes
app.blueprint(health_bp)
app.config.HEALTH_CHECK_LIST = [RedisCheck] # Can implement subclasses of BaseHealthCheck
# Configure health check service
@app.before_server_start
async def setup_health_checks(app, loop):
"""Set up health check dependencies"""
from redis.asyncio import Redis
# Redis client
app.ctx.redis = Redis.from_url(config.REDIS_URL)
# PostgreSQL connection pool
# app.ctx.pg = await asyncpg.create_pool(config.DATABASE_URL)
Logging Setup¶
import logging
from sanic.log import logger
# Configure logging
logging.basicConfig(
level=logging.INFO if not config.DEBUG else logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler("app.log"),
logging.StreamHandler()
]
)
# Use in code
logger.info("Application started")
logger.error("An error occurred", exc_info=True)
Exception Handling¶
from sanic.exceptions import NotFound, InvalidUsage
from srf.views.http_status import HTTPStatus
from sanic.response import json
@app.exception(NotFound)
async def handle_not_found(request, exception):
"""Handle 404 errors"""
return json(
{"error": "Resource not found", "message": str(exception)},
status=HTTPStatus.HTTP_404_NOT_FOUND
)
@app.exception(InvalidUsage)
async def handle_invalid_usage(request, exception):
"""Handle 400 errors"""
return json(
{"error": "Invalid request", "message": str(exception)},
status=HTTPStatus.HTTP_400_BAD_REQUEST
)
@app.exception(Exception)
async def handle_exception(request, exception):
"""Handle uncaught exceptions"""
logger.error(f"Unhandled exception: {exception}", exc_info=True)
return json(
{"error": "Server internal error"},
status=HTTPStatus.HTTP_500_INTERNAL_SERVER_ERROR
)
Full Application Example¶
app.py:
from sanic import Sanic
from sanic_cors import CORS
from tortoise.contrib.sanic import register_tortoise
from srf.config import settings
from srf.middleware.authmiddleware import set_user_to_request_ctx
from srf.health.route import bp as health_bp
from config import config
from routes import register_routes
import logging
# Create application
app = Sanic("MyApp")
# Apply configuration
app.config.update_config(config)
settings.set_app(app)
# Configure CORS
CORS(app, origins=config.CORS_ORIGINS)
# Configure logging
logging.basicConfig(level=logging.INFO)
# Register database
register_tortoise(
app,
db_url=config.DATABASE_URL,
modules={"models": ["models"]},
generate_schemas=True,
)
# Register middleware
@app.middleware("request")
async def auth_middleware(request):
await set_user_to_request_ctx(request)
# Register routes
register_routes(app)
app.blueprint(health_bp)
# Exception handling
@app.exception(Exception)
async def handle_exception(request, exception):
from sanic.response import json
logging.error(f"Error: {exception}", exc_info=True)
return json({"error": str(exception)}, status=500)
if __name__ == "__main__":
app.run(
host=getattr(config, "HOST", "0.0.0.0"),
port=getattr(config, "PORT", 8000),
debug=config.DEBUG,
auto_reload=config.DEBUG,
)
Production Deployment¶
Using Gunicorn¶
Using Docker¶
Create Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
Create docker-compose.yml:
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- ENVIRONMENT=production
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
db:
image: postgres:14
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
Run:
Best Practices¶
- Use environment variables: Do not hardcode sensitive information in code
- Separate configurations: Create different configurations for different environments
- Use migrations: Manage database changes through migrations
- Log important actions and errors
- Handle exceptions globally: Return friendly error messages
- Provide health checks: Offer endpoints for monitoring systems
- Implement rate limiting: Prevent API abuse
- Configure CORS properly: Set up cross-origin access correctly
Next Steps¶
- Learn about Core Concepts to understand SRF's features in depth
- View Configuration Options to learn about all available configurations
- Read API Reference for detailed API documentation