CtrlK
BlogDocsLog inGet started
Tessl Logo

litestar-project-starter

Scaffold a production-ready Litestar 2.x application with class-based controllers, DTOs, guards, SQLAlchemy plugin, and auto-generated OpenAPI docs.

73

Quality

64%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./backend-python/litestar-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Litestar Project Starter

Scaffold a production-ready Litestar 2.x application with class-based controllers, DTOs, guards, SQLAlchemy plugin, and auto-generated OpenAPI docs.

Prerequisites

  • Python 3.12+
  • PostgreSQL 15+ (for asyncpg)
  • uv or pip for dependency management

Scaffold Command

mkdir -p src/app/{controllers,models,dtos,guards,middleware,services,db} tests
touch src/app/__init__.py src/app/controllers/__init__.py src/app/models/__init__.py \
      src/app/dtos/__init__.py src/app/guards/__init__.py src/app/middleware/__init__.py \
      src/app/services/__init__.py src/app/db/__init__.py tests/__init__.py

python -m venv .venv && source .venv/bin/activate

cat > pyproject.toml << 'PYPROJECT'
[project]
name = "app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "litestar[standard]>=2.14",
    "advanced-alchemy>=0.28",
    "asyncpg>=0.30",
    "pydantic>=2.9",
    "pydantic-settings>=2.6",
    "structlog>=24.4",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.3",
    "pytest-asyncio>=0.24",
    "ruff>=0.8",
]

[tool.ruff]
target-version = "py312"
line-length = 99

[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM", "ASYNC"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
PYPROJECT

Project Structure

project-root/
├── pyproject.toml
├── .env.example                    # Required env vars template
├── src/
│   └── app/
│       ├── __init__.py
│       ├── main.py              # Application factory + lifespan
│       ├── config.py            # Pydantic Settings
│       ├── controllers/
│       │   ├── __init__.py
│       │   ├── health.py
│       │   └── users.py
│       ├── models/
│       │   ├── __init__.py
│       │   └── user.py          # SQLAlchemy ORM models
│       ├── dtos/
│       │   ├── __init__.py
│       │   └── user.py          # Litestar DTOs
│       ├── guards/
│       │   ├── __init__.py
│       │   └── auth.py
│       ├── middleware/
│       │   ├── __init__.py
│       │   └── logging.py
│       ├── services/
│       │   ├── __init__.py
│       │   └── user.py
│       └── db/
│           ├── __init__.py
│           └── plugin.py        # SQLAlchemy plugin config
└── tests/
    ├── __init__.py
    ├── conftest.py
    └── test_users.py

Key Conventions

  • Class-based controllers are the primary routing mechanism. One controller per resource.
  • DTOs control serialization/deserialization. Use Litestar's built-in DTO system, not manual Pydantic schemas.
  • Guards enforce authorization. Attach them at the controller or handler level.
  • Dependency injection is first-class. Use Provide to register dependencies and type hints to inject them.
  • The SQLAlchemy plugin (via advanced-alchemy) manages engine, sessions, and repository pattern.
  • OpenAPI docs are generated automatically at /schema (Swagger UI, ReDoc, Stoplight Elements all available).
  • Lifespan hooks handle startup/shutdown logic (DB connections, logging setup).

Essential Patterns

Configuration (config.py)

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

    app_name: str = "app"
    debug: bool = False
    database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/app"
    cors_origins: list[str] = ["http://localhost:3000"]
    secret_key: str = "change-me-in-production"


settings = Settings()

ORM Model (models/user.py)

from datetime import datetime
from uuid import UUID

from advanced_alchemy.base import UUIDAuditBase
from sqlalchemy.orm import Mapped, mapped_column


class User(UUIDAuditBase):
    __tablename__ = "users"

    email: Mapped[str] = mapped_column(unique=True, index=True)
    name: Mapped[str]
    is_active: Mapped[bool] = mapped_column(default=True)

DTOs (dtos/user.py)

from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO, SQLAlchemyDTOConfig

from app.models.user import User


class UserReadDTO(SQLAlchemyDTO[User]):
    config = SQLAlchemyDTOConfig(
        exclude={"updated_at"},
    )


class UserCreateDTO(SQLAlchemyDTO[User]):
    config = SQLAlchemyDTOConfig(
        include={"email", "name"},
    )


class UserUpdateDTO(SQLAlchemyDTO[User]):
    config = SQLAlchemyDTOConfig(
        include={"name", "is_active"},
        partial=True,
    )

Service / Repository (services/user.py)

from advanced_alchemy.repository import SQLAlchemyAsyncRepository
from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.user import User


class UserRepository(SQLAlchemyAsyncRepository[User]):
    model_type = User


class UserService(SQLAlchemyAsyncRepositoryService[User]):
    repository_type = UserRepository


async def provide_user_service(db_session: AsyncSession) -> UserService:
    return UserService(session=db_session)

Guard (guards/auth.py)

from litestar.connection import ASGIConnection
from litestar.exceptions import NotAuthorizedException
from litestar.handlers import BaseRouteHandler


def require_active_user(connection: ASGIConnection, _: BaseRouteHandler) -> None:
    """Guard that ensures the request has a valid authenticated user."""
    if not connection.scope.get("user"):
        raise NotAuthorizedException(detail="Authentication required")

Controller (controllers/users.py)

from uuid import UUID

from litestar import Controller, delete, get, patch, post
from litestar.di import Provide
from litestar.dto import DTOData
from litestar.params import Parameter

from app.dtos.user import UserCreateDTO, UserReadDTO, UserUpdateDTO
from app.models.user import User
from app.services.user import UserService, provide_user_service


class UserController(Controller):
    path = "/users"
    tags = ["Users"]
    dependencies = {"user_service": Provide(provide_user_service)}
    return_dto = UserReadDTO

    @get("/")
    async def list_users(self, user_service: UserService) -> list[User]:
        results, _ = await user_service.list_and_count()
        return results

    @get("/{user_id:uuid}")
    async def get_user(self, user_service: UserService, user_id: UUID) -> User:
        return await user_service.get(user_id)

    @post("/", dto=UserCreateDTO)
    async def create_user(self, user_service: UserService, data: User) -> User:
        return await user_service.create(data)

    @patch("/{user_id:uuid}", dto=UserUpdateDTO)
    async def update_user(
        self, user_service: UserService, user_id: UUID, data: DTOData[User]
    ) -> User:
        return await user_service.update(item_id=user_id, data=data.as_builtins())

    @delete("/{user_id:uuid}")
    async def delete_user(self, user_service: UserService, user_id: UUID) -> None:
        await user_service.delete(user_id)

Health Controller (controllers/health.py)

from litestar import Controller, get


class HealthController(Controller):
    path = "/health"
    tags = ["Health"]

    @get("/")
    async def health_check(self) -> dict[str, str]:
        return {"status": "ok"}

SQLAlchemy Plugin (db/plugin.py)

from advanced_alchemy.extensions.litestar import (
    AsyncSessionConfig,
    SQLAlchemyAsyncConfig,
    SQLAlchemyPlugin,
)

from app.config import settings

sqlalchemy_config = SQLAlchemyAsyncConfig(
    connection_string=settings.database_url,
    session_config=AsyncSessionConfig(expire_on_commit=False),
)

sqlalchemy_plugin = SQLAlchemyPlugin(config=sqlalchemy_config)

Middleware (middleware/logging.py)

from litestar.middleware import AbstractMiddleware
from litestar.types import ASGIApp, Receive, Scope, Send

import structlog

logger = structlog.get_logger()


class RequestLoggingMiddleware(AbstractMiddleware):
    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] == "http":
            await logger.ainfo(
                "request_started",
                method=scope["method"],
                path=scope["path"],
            )
        await self.app(scope, receive, send)

Exception Handler

from litestar import Request, Response
from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR

import structlog

logger = structlog.get_logger()


async def internal_server_error_handler(request: Request, exc: Exception) -> Response:
    logger.error("unhandled_exception", path=request.url.path, error=str(exc))
    return Response(
        content={"error": "Internal Server Error"},
        status_code=HTTP_500_INTERNAL_SERVER_ERROR,
    )

Application Factory (main.py)

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

import structlog
from litestar import Litestar, Request, Response
from litestar.config.cors import CORSConfig
from litestar.openapi import OpenAPIConfig
from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR

from app.config import settings
from app.controllers.health import HealthController
from app.controllers.users import UserController
from app.db.plugin import sqlalchemy_plugin
from app.middleware.logging import RequestLoggingMiddleware

logger = structlog.get_logger()


async def internal_server_error_handler(request: Request, exc: Exception) -> Response:
    logger.error("unhandled_exception", path=request.url.path, error=str(exc))
    return Response(
        content={"error": "Internal Server Error"},
        status_code=HTTP_500_INTERNAL_SERVER_ERROR,
    )


@asynccontextmanager
async def lifespan(app: Litestar) -> AsyncGenerator[None, None]:
    logger.info("application_startup", app_name=settings.app_name)
    yield
    logger.info("application_shutdown")


app = Litestar(
    route_handlers=[
        HealthController,
        UserController,
    ],
    plugins=[sqlalchemy_plugin],
    middleware=[RequestLoggingMiddleware],
    openapi_config=OpenAPIConfig(
        title=settings.app_name,
        version="0.1.0",
        path="/schema",
    ),
    cors_config=CORSConfig(allow_origins=settings.cors_origins),
    lifespan=[lifespan],
    exception_handlers={500: internal_server_error_handler},
    debug=settings.debug,
)

Test Conftest (tests/conftest.py)

import pytest
from litestar.testing import AsyncTestClient

from app.main import app


@pytest.fixture
async def client():
    async with AsyncTestClient(app=app) as client:
        yield client

Test Example (tests/test_users.py)

import pytest


@pytest.mark.asyncio
async def test_health(client):
    response = await client.get("/health/")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}


@pytest.mark.asyncio
async def test_create_user(client):
    response = await client.post("/users/", json={
        "email": "new@example.com",
        "name": "New User",
    })
    assert response.status_code == 201
    data = response.json()
    assert data["email"] == "new@example.com"

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Create virtual environment: python -m venv .venv && source .venv/bin/activate
  3. Install dependencies: pip install -e ".[dev]" (or uv sync)
  4. Run database migrations: alembic upgrade head
  5. Start dev server: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 --app-dir src
  6. Test the API: curl http://localhost:8000/schema/swagger

Common Commands

# Install dependencies
uv sync                      # or: pip install -e ".[dev]"

# Run development server
litestar --app src/app/main:app run --reload --host 0.0.0.0 --port 8000

# Or with uvicorn directly
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 --app-dir src

# Database migrations (via advanced-alchemy CLI or manual Alembic)
# advanced-alchemy uses Alembic under the hood
alembic revision --autogenerate -m "add users table"
alembic upgrade head

# Run tests
pytest

# Lint and format
ruff check src tests
ruff format src tests

# View OpenAPI docs
# Swagger UI:         http://localhost:8000/schema/swagger
# ReDoc:              http://localhost:8000/schema/redoc
# Stoplight Elements: http://localhost:8000/schema/elements

Integration Notes

  • Frontend: Litestar auto-generates OpenAPI at /schema/openapi.json. Use codegen tools (openapi-typescript-codegen, orval) to produce typed clients.
  • Docker: Use uvicorn app.main:app --host 0.0.0.0 --port 8000 as the entrypoint. For multiple workers, use gunicorn -k uvicorn.workers.UvicornWorker.
  • CI: Run ruff check, pytest, and alembic upgrade head (against a test DB) in the pipeline.
  • Background Tasks: Litestar has built-in BackgroundTask and BackgroundTasks support on response objects. For heavier workloads, pair with arq or SAQ.
  • Caching: Use litestar.config.cache.CacheConfig with a Redis backend for response caching.
  • Authentication: Litestar provides AbstractAuthenticationMiddleware for custom JWT/session auth. Combine with guards for per-route authorization.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.