Getting Started
This guide will walk you through building a complete, production-ready FastAPI application using ZodiacCore in 5 minutes.
1. Generate Your Project
The easiest way to get started is using the zodiac CLI to scaffold a complete project structure. The CLI must be run from within a project directory (see Installation).
Note: Replace
my_appwith your desired project name.
# Create a project directory and initialize with uv
mkdir my_app
cd my_app
uv init --python 3.12
# Install the CLI and SQL support (required by the standard-3tier template)
uv add "zodiac-core[zodiac,sql]"
# Generate the project (output to parent dir, allow generation into the existing directory with --force)
zodiac new my_app --tpl standard-3tier -o .. --force
# Add FastAPI CLI for running the app
uv add "fastapi[standard]" --dev
# Run the application
uv run fastapi run --reload
The standard-3tier template generates a project following the Standard 3-Tier Layered Architecture with Dependency Injection:
- API (Presentation): FastAPI routers and request/response handling
- Application (Logic): Business logic and use case orchestration
- Infrastructure (Implementation): Database models, repositories, and external integrations
The project uses dependency-injector for managing component dependencies, providing clean separation of concerns and making the codebase more testable and maintainable.
Note: While the template uses a 3-tier architecture, ZodiacCore supports flexible layered architectures. You can extend it to a 4-tier architecture with a Domain layer when needed. See the Architecture Guide for details.
2. Project Structure
After generation, your project will have this structure:
my_app/
├── app/
│ ├── api/ # Presentation layer (routers, schemas)
│ │ ├── routers/
│ │ └── schemas/
│ ├── application/ # Business logic layer (services)
│ │ └── services/
│ ├── infrastructure/ # Implementation layer (DB, external clients)
│ │ ├── database/
│ │ └── external/
│ └── core/ # DI container and configuration
├── config/ # Environment-based configuration files
├── tests/ # Test suite
└── main.py # Application entry point
3. Understanding the Architecture
Dependency Injection Container
The project uses dependency-injector to manage dependencies. The container is defined in app/core/container.py.
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration(strict=True)
# Infrastructure layer
item_repository = providers.Factory(ItemRepository)
# Application layer
item_service = providers.Factory(
ItemService,
item_repo=item_repository,
)
Dependencies are injected into routers using FastAPI's Depends. In main.py, the generated project uses Container.initialize([config_dir]), which loads config from .ini and wires all router modules (files named *_router.py) under app.api.routers; when you add a new router file, it is picked up automatically.
from dependency_injector.wiring import Provide, inject
from fastapi import Depends
@router.get("")
@inject
async def list_items(
service: Annotated[ItemService, Depends(Provide[Container.item_service])],
):
return await service.list_items(params)
Professional Pagination with paginate_query
The generated project uses BaseSQLRepository.paginate_query() for pagination, which automatically handles:
- Session management
- Total count calculation
- Limit/offset application
- Response packaging
Repository Example:
from sqlalchemy import select
from zodiac_core.db.repository import BaseSQLRepository
from zodiac_core.pagination import PagedResponse, PageParams
class ItemRepository(BaseSQLRepository):
async def list_items(self, params: PageParams) -> PagedResponse[ItemModel]:
"""List items with pagination using BaseSQLRepository.paginate_query."""
stmt = select(ItemModel).order_by(ItemModel.id)
return await self.paginate_query(stmt, params)
Service Example:
class ItemService:
def __init__(self, item_repo: ItemRepository) -> None:
self.item_repo = item_repo
async def list_items(self, page_params: PageParams) -> PagedResponse[ItemModel]:
"""List items with pagination."""
return await self.item_repo.list_items(page_params)
Router Example:
@router.get("", response_model=PagedResponse[ItemSchema])
@inject
async def list_items(
page_params: Annotated[PageParams, Depends()],
service: Annotated[ItemService, Depends(Provide[Container.item_service])],
):
"""List items with pagination."""
return await service.list_items(page_params)
No manual skip/limit calculations needed! The paginate_query method handles everything automatically.
4. Configuration
The project uses file-based configuration. Configuration files are in the config/ directory:
config/app.ini- Base configurationconfig/app.develop.ini- Development overridesconfig/app.testing.ini- Test overrides you can add for pytest or local integration testsconfig/app.production.ini- Production overrides you can add when needed
The generated template loads configuration from the APPLICATION_ENVIRONMENT environment variable and defaults to develop when it is unset:
from pathlib import Path
from zodiac_core.config import ConfigManagement
config_dir = Path(__file__).resolve().parent.parent / "config"
config_files = ConfigManagement.get_config_files(
search_paths=[config_dir],
env_var="APPLICATION_ENVIRONMENT",
default_env="develop",
)
for path in config_files:
container.config.from_ini(path, required=True)
For tests, it is recommended to add config/app.testing.ini and set:
5. Standard Response Wrapper
When you use Zodiac's APIRouter, all successful responses are automatically wrapped in a standard structure:
@router.get("/items/{item_id}")
async def get_item(item_id: int):
return {"id": 1, "name": "Example"}
The resulting JSON will be:
6. Handling Exceptions
Raise ZodiacException subclasses for automatic error handling:
from zodiac_core.exceptions import NotFoundException, BadRequestException
@router.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id == 0:
raise BadRequestException(message="Item ID cannot be zero")
item = await service.get_by_id(item_id)
if not item:
raise NotFoundException(message=f"Item {item_id} not found")
return item
The response will automatically follow the standard error format:
7. Running Your Application
The scaffolding flow in Step 1 already includes uv run fastapi run --reload. If you prefer uvicorn or need to re-sync dependencies:
Or use uv run fastapi run --reload (requires fastapi[standard]).
Visit:
- API Documentation: http://127.0.0.1:8000/docs
- Health Check: http://127.0.0.1:8000/api/v1/health
Summary
You now have a fully functional application with:
- ✅ 3-Tier Layered Architecture with Dependency Injection
- ✅ Professional Pagination using
paginate_query - ✅ Structured Logging with trace ID propagation
- ✅ Standard Error Handling with automatic response wrapping
- ✅ File-based Configuration for different environments
- ✅ Async Database Sessions with SQLModel
Next Steps
- Architecture Guide — Layered design, DI container, and project structure
- API Reference — Config, database, pagination, routing, schemas, and more
- CLI Documentation — Scaffold new projects and modules with
zodiac new