Skip to main content

Auto-generate FastAPI CRUD endpoints from SQLModel models

Project description

auen

Auto-generate FastAPI CRUD endpoints from SQLModel models.

Why auen?

Today writing repetitive code, like CRUD endpoints, became a single prompt to your Agent of Choice. So, why should we generate them from code instead? I believe, this is about standardization: Having the piece of mind that there is no artistic freedom in your CRUD endpoints, you'll expose to internal or external users. The ultimate goal here is to let you focus on the development of a data model beneath and let the interaction layer appear for free, at least for the set of endpoints that don't need any further customization.

Installation

pip install auen

Quickstart

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import Field, SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession

from auen import CrudRouterSpec, build_crud_routers

engine = create_async_engine("sqlite+aiosqlite:///bookstore.db")


class Author(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    bio: str | None = None


class Book(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    isbn: str
    author_id: int | None = Field(default=None, foreign_key="author.id")


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)
    yield


async def get_session() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSession(engine) as session:
        yield session


app = FastAPI(lifespan=lifespan)
routers = build_crud_routers(
    specs=[
        CrudRouterSpec(model=Author),
        CrudRouterSpec(model=Book),
    ],
    session_dep=get_session,
)
for router in routers:
    app.include_router(router)

This spec-based interface is the canonical style. CrudRouterBuilder remains available as a fluent compatibility adapter.

from auen import CrudRouterSpec, Operation, build_crud_routers

routers = build_crud_routers(
    specs=[
        CrudRouterSpec(model=Author, operations={Operation.LIST, Operation.READ}),
        CrudRouterSpec(model=Book, operations={Operation.CREATE, Operation.LIST}),
    ],
    session_dep=get_session,
)
for router in routers:
    app.include_router(router)

Without auen, this requires writing five endpoint functions per model, request/response models, query parameter handling, error responses, and database session management — roughly 150 lines of code that looks the same in every project.

With auen, you get:

Method Path Description Method Path Description
POST /authors/ Create an author POST /books/ Create a book
GET /authors/ List authors GET /books/ List books
GET /authors/{id} Get an author GET /books/{id} Get a book
PATCH /authors/{id} Update an author PATCH /books/{id} Update a book
DELETE /authors/{id} Delete an author DELETE /books/{id} Delete a book

Explicit Schemas

For production, define explicit schemas to control exactly which fields are exposed. Without auen, you'd maintain these schemas and write all the endpoint code that uses them — auen handles the latter.

If you want fully derived schema bundles (including nested payload models), use:

from auen import derive_schemas

schemas = derive_schemas(Book)
# schemas.create / schemas.read / schemas.update
# schemas.nested_create / schemas.nested_update

For advanced use cases, you can access the full schema container directly:

from auen import derive_schema_container

container = derive_schema_container(Book)
# container.nested_update is used by PATCH /{id}/nested

Authentication

Auth integration is a common copy-paste pattern — every project writes the same dependency injection and per-operation gating. auen standardizes it.

from fastapi import Header, HTTPException
from auen import AuthConfig, CrudRouterBuilder


def get_current_user(authorization: str = Header()) -> str:
    if not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401)
    return authorization[7:]  # return user identity


app.include_router(
    CrudRouterBuilder()
    .with_auth(AuthConfig(dependency=get_current_user))
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Row-Level Policies

Row-level security is frequently reimplemented incorrectly — a missed filter on the LIST endpoint leaks data. auen makes it declarative so the policy is applied consistently across all operations.

from auen import AuthConfig, CrudRouterBuilder


class OwnerPolicy:
    def can_create(self, user, obj_in):
        return True

    def can_read(self, user, db_obj):
        return db_obj.author_id == user

    def can_update(self, user, db_obj, obj_in):
        return db_obj.author_id == user

    def can_delete(self, user, db_obj):
        return db_obj.author_id == user

    def filter_list_query(self, user, query):
        return query.where(Book.author_id == user)


app.include_router(
    CrudRouterBuilder()
    .with_auth(AuthConfig(dependency=get_current_user))
    .with_policy(OwnerPolicy())
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Lifecycle Hooks

from auen import CrudRouterBuilder, HooksConfig


async def notify_new_book(session, book_obj, current_user):
    ...  # send email, log event, publish to queue, etc.


async def validate_isbn(session, obj_in, current_user):
    ...  # raise HTTPException to abort


app.include_router(
    CrudRouterBuilder()
    .with_hooks(
        HooksConfig(
            before_create=validate_isbn,
            after_create=notify_new_book,
        )
    )
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Pagination & Filtering

Pagination and filtering are boilerplate that every API writes identically — offset/limit handling, sort-field validation, operator mapping. auen generates it from a declaration.

from auen import CrudRouterBuilder, PaginationConfig, FilterConfig, FilterFieldConfig

app.include_router(
    CrudRouterBuilder()
    .with_pagination(PaginationConfig(default_limit=20, max_limit=100))
    .with_filters(
        FilterConfig(
            fields={"title": FilterFieldConfig(ops=frozenset({"eq"}))},
            sort_fields=["title", "isbn"],
        )
    )
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Selecting Operations

from auen import CrudRouterBuilder, Operation

app.include_router(
    CrudRouterBuilder()
    .with_operations({Operation.LIST, Operation.READ})  # read-only API
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Development

# Install dependencies
uv sync

# Run tests
uv run pytest

# Format
uv run ruff format .

# Lint
uv run ruff check .

# Type check
uv run ty check

License

MIT

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

auen-0.6.0.tar.gz (149.3 kB view details)

Uploaded Source

Built Distribution

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

auen-0.6.0-py3-none-any.whl (32.4 kB view details)

Uploaded Python 3

File details

Details for the file auen-0.6.0.tar.gz.

File metadata

  • Download URL: auen-0.6.0.tar.gz
  • Upload date:
  • Size: 149.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for auen-0.6.0.tar.gz
Algorithm Hash digest
SHA256 bbd7e69cba774476d15df512e7d53ea2ceb69597925f329e5ea4d0c85e06ad9a
MD5 34ec316fa60ee7cbbaf9cd899cbdc14a
BLAKE2b-256 afe36dadc77752b3a73f3e993ffc218b58fdd1d82de2469d0a93eb8ac0a78898

See more details on using hashes here.

File details

Details for the file auen-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: auen-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 32.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for auen-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 783898fe70b38567ab2610b6da2282163fe32c944e5516d2912ecaf7809a5f2c
MD5 350f982bc5d2db6609d5b8dc544e5081
BLAKE2b-256 45a9df90a09fdeb51b92aefbdc9b79bf1af6055f9f76f642755232a4c8e80d7c

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