Skip to content

Configuration Guide

LightAPI v2 is configured through Python code or a lightapi.yaml file. There are no magic environment-variable-only globals — every option is explicit.

Python Configuration

LightApi(...) constructor

from sqlalchemy import create_engine
from lightapi import LightApi, Middleware

engine = create_engine("sqlite:///app.db")


def my_login_validator(username: str, password: str):
    if username == "admin" and password == "secret":
        return {"sub": "1", "username": "admin", "is_admin": True}
    return None


app = LightApi(
    engine=engine,                          # SQLAlchemy Engine (sync or async)
    cors_origins=["https://myapp.com"],     # CORS allowed origins
    middlewares=[MyMiddleware],             # List of Middleware subclasses
    login_validator=my_login_validator,     # Used by auto-registered /auth/login
)
Parameter Type Default Description
engine Engine \| AsyncEngine — SQLAlchemy engine. If omitted, uses database_url or env vars.
database_url str \| None — Creates a sync engine from this URL if engine is not given. Falls back to LIGHTAPI_DATABASE_URL env var. Raises ConfigurationError if none are provided.
mode str \| None auto "sync" or "async". Auto-detected from the engine type and from async def overrides if omitted.
cors_origins list[str] [] Domains allowed for cross-origin requests.
middlewares list[type] [] Middleware subclasses applied to every request.
auth_path str "/auth" Base path for the auto-registered login route.
session_manager SessionManager \| None — Override the default session manager (advanced).
rate_limiter RateLimiter \| dict \| None — Rate-limit config applied to /auth/login.
login_validator Callable[[str, str], dict \| None] — Credential validator used by /auth/login. Returns the JWT payload on success or None.
use_test_isolation bool False Mount endpoints onto unique table names and per-thread metadata for tests.

Registering endpoints

from lightapi import LightApi, RestEndpoint, Field

class UserEndpoint(RestEndpoint):
    username: str = Field(min_length=3)
    email: str

app = LightApi(engine=engine)
app.register({
    "/users": UserEndpoint,
    "/posts": PostEndpoint,
})

register() accepts a dict[str, type] mapping URL prefixes to RestEndpoint subclasses. It maps each class onto a SQLAlchemy table and registers both collection (/users) and detail (/users/{id}) routes. Tables are created when you call build_app() or run().

When at least one endpoint declares Authentication(backend=JWTAuthentication) or Authentication(backend=BasicAuthentication), register() also wires up POST /auth/login (and POST /auth/token) using the login_validator you passed to LightApi(...).

Running the server

app.run(host="0.0.0.0", port=8000, debug=False, reload=False)

Or as an ASGI app (e.g. with Gunicorn + Uvicorn workers):

asgi_app = app.build_app()

YAML Configuration

Use LightApi.from_config() to bootstrap from a YAML file. The YAML schema is validated by Pydantic v2 at load time, so any typo or missing field produces a clear ConfigurationError message rather than a cryptic runtime crash.

Two formats are supported:

Define every endpoint — fields, HTTP methods, auth, filtering, pagination — entirely in YAML. No RestEndpoint subclasses required.

# lightapi.yaml
database:
  url: "${DATABASE_URL}"           # ${VAR} env-var substitution

cors_origins:
  - "https://myapp.com"

# Defaults applied to every endpoint unless overridden
defaults:
  authentication:
    backend: JWTAuthentication
    permission: IsAuthenticated
  pagination:
    style: page_number
    page_size: 20

middleware:
  - CORSMiddleware

endpoints:
  - route: /users
    fields:
      username: { type: str, min_length: 3, max_length: 150 }
      email:    { type: str }
      is_admin: { type: bool, default: false, optional: true }
    meta:
      methods: [GET, POST, PUT, DELETE]
      filtering:
        fields:   [is_admin]
        search:   [username, email]
        ordering: [username]

  - route: /posts
    fields:
      title:     { type: str, max_length: 255 }
      published: { type: bool, default: false }
    meta:
      methods: [GET, POST]
      authentication:
        permission: AllowAny    # override the global default for this endpoint
      pagination:
        page_size: 10
from lightapi import LightApi

app = LightApi.from_config("lightapi.yaml")
app.run()
# lightapi.yaml — all features in one file
mode: sync    # or "async" for AsyncEngine

database:
  url: "${DATABASE_URL}"

cors_origins:
  - "https://myapp.com"

auth:
  auth_path: /v1/auth
  login_validator: myapp.validators.login   # dotted path to sync callable

defaults:
  authentication:
    backend: JWTAuthentication
    permission: IsAuthenticated
    jwt_expiration: 3600
  pagination:
    style: page_number
    page_size: 20

middleware:
  - myapp.middleware.RequestIdMiddleware

endpoints:
  # ── Public read, authenticated write ──────────────────────────────────────
  - route: /articles
    fields:
      title:    { type: str, min_length: 1, max_length: 200 }
      category: { type: str, min_length: 1 }
      price:    { type: float, ge: 0, default: 0 }
      featured: { type: bool, default: false }
      note:     { type: str, optional: true }
    meta:
      methods:
        GET:
          authentication: { permission: AllowAny }
        POST:
          authentication: { permission: IsAuthenticated }
        PUT:
          authentication: { permission: IsAuthenticated }
        PATCH:
          authentication: { permission: IsAuthenticated }
        DELETE:
          authentication: { permission: IsAdminUser }
      authentication:
        backend: JWTAuthentication
      filtering:
        fields:   [category, featured]
        search:   [title]
        ordering: [price, title]
      pagination:
        style: page_number
        page_size: 10
      cache:
        ttl: 60      # GET responses cached 60 s; invalidated on writes
      serializer:
        read:  [id, title, category, price, featured, version]
        write: [id, title, category, price, featured, note, version]

  # ── Fully protected with cursor pagination ────────────────────────────────
  - route: /orders
    fields:
      reference: { type: str, min_length: 1 }
      total:     { type: float, ge: 0 }
    meta:
      methods: [GET, POST]
      pagination:
        style: cursor
        page_size: 50

  # ── Reflected legacy table ────────────────────────────────────────────────
  - route: /legacy
    reflect: true
    meta:
      table: legacy_orders
      methods: [GET]
      authentication:
        permission: AllowAny
from lightapi import LightApi

app = LightApi.from_config("lightapi.yaml")
app.run()

YAML schema reference

Field Type Description
database.url string SQLAlchemy URL. Supports ${VAR} substitution.
cors_origins list CORS allowed origins.
defaults.authentication.backend string Auth backend class name (e.g. JWTAuthentication).
defaults.authentication.permission string Permission class name (e.g. IsAuthenticated).
defaults.pagination.style string page_number or cursor.
defaults.pagination.page_size int Rows per page.
middleware list Class names or dotted import paths resolved at startup.
endpoints[].route string URL prefix.
endpoints[].fields object Inline field definitions (see below).
endpoints[].meta.methods list or dict HTTP verbs to enable. Dict form allows per-method auth.
endpoints[].meta.authentication object Overrides defaults.authentication for this endpoint.
endpoints[].meta.filtering object fields, search, ordering lists. Backends are auto-selected.
endpoints[].meta.pagination object style + page_size for this endpoint.
mode string "sync" or "async". Auto-detected when omitted.
endpoints[].meta.cache object { ttl: N } — cache GET responses for N seconds. Requires Redis; silently skipped if unreachable. Writes (POST/PUT/PATCH/DELETE) invalidate the cache.
endpoints[].meta.serializer object { fields: [...] } for unified projection, or { read: [...], write: [...] } for per-verb projection.
endpoints[].meta.table string Custom table name. Required when reflect: true to specify which existing table to map.
endpoints[].meta.filtering.backends list Explicit list of backend class names (FieldFilter, SearchFilter, OrderingFilter). Auto-selected from non-empty fields/search/ordering when omitted.
auth.auth_path string Path prefix for the auto-registered login routes (default /auth → /auth/login).
auth.login_validator string Dotted import path to a sync (username, password) → dict \| None function. Exceptions are treated as None (→ 401).
endpoints[].reflect bool Reflect an existing table instead of creating one.

Field definition keys

Each key under endpoints[].fields maps a column name to:

Key Type Description
type string str, int, float, bool, datetime, Decimal
optional bool Whether the field is Optional (default false).
default any Default value passed to Field().
min_length / max_length int String length constraints.
gt / ge / lt / le number Numeric comparison constraints.
pattern string Regex pattern for strings.
index bool Create a database index.
unique bool Add a UNIQUE constraint.
foreign_key string "table.col" — adds a SQLAlchemy ForeignKey constraint.
decimal_places int A Decimal with more than N decimal places is rejected (422); the column type stays NUMERIC.

Cache invalidation

When meta.cache is set, successful GET (list and detail) responses are stored in Redis. Any write request (POST, PUT, PATCH, DELETE) on the same endpoint automatically invalidates the cached entries for that endpoint's key prefix — no manual invalidation required.

meta:
  cache:
    ttl: 300   # GET cached for 5 minutes; auto-invalidated on writes

If Redis is unreachable at startup, a RuntimeWarning is emitted and caching is silently skipped for the lifetime of the process.

Environment variable substitution

Any ${VAR} placeholder in a URL field is replaced with the matching environment variable at load time. A missing variable raises ConfigurationError immediately:

database:
  url: "${DATABASE_URL}"

Per-method authentication

Use the dict form of methods to set different permission classes per HTTP verb:

endpoints:
  - route: /articles
    fields:
      title:   { type: str }
      content: { type: str }
    meta:
      methods:
        GET:
          authentication: { permission: AllowAny }
        POST:
          authentication: { permission: IsAuthenticated }
        DELETE:
          authentication: { permission: IsAdminUser }
      authentication:
        backend: JWTAuthentication   # shared backend for all methods

Async engine

Switching to async I/O requires only changing the engine:

from sqlalchemy.ext.asyncio import create_async_engine
from lightapi import LightApi

engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
app = LightApi(engine=engine)

Install the async extras first:

uv add "lightapi[async]"

See Async Support for the full guide.

CORS

Pass a list of allowed origins to enable CORS headers on every response:

app = LightApi(
    engine=engine,
    cors_origins=["https://myapp.com", "https://admin.myapp.com"],
)

Use ["*"] to allow all origins (not recommended for production).

Middleware

Register middleware globally at startup:

from lightapi import LightApi, Middleware
from starlette.requests import Request
from starlette.responses import Response

class RequestLogMiddleware(Middleware):
    def process(
        self, request: Request, response: Response | None
    ) -> Response | None:
        if response is None:
            # pre-request phase
            print(f"→ {request.method} {request.url.path}")
        else:
            # post-response phase
            print(f"← {response.status_code} {request.url.path}")
        return response

app = LightApi(engine=engine, middlewares=[RequestLogMiddleware])

See Middleware for detailed documentation.

Environment variables

LightAPI reads the following environment variables:

Variable Description
LIGHTAPI_DATABASE_URL Database URL when no engine or database_url is passed. Required when neither is provided.
LIGHTAPI_JWT_SECRET Secret key used by JWTAuthentication. Required when JWT auth is used.

Per-endpoint configuration (Meta)

Each RestEndpoint subclass can have an inner Meta class that controls its behaviour:

from lightapi import (
    RestEndpoint, Field,
    Authentication, Filtering, Pagination, Serializer, Cache,
    JWTAuthentication, IsAuthenticated,
    FieldFilter, SearchFilter, OrderingFilter,
    RedisCache,
)

class ArticleEndpoint(RestEndpoint):
    title: str
    body: str
    published: bool = Field(default=False)

    class Meta:
        authentication = Authentication(
            backend=JWTAuthentication,
            permission=IsAuthenticated,
        )
        filtering = Filtering(
            backends=[FieldFilter, SearchFilter, OrderingFilter],
            fields=["published"],
            search=["title", "body"],
            ordering=["title", "created_at"],
        )
        pagination = Pagination(style="page_number", page_size=20)
        serializer = Serializer(read=["id", "title", "published", "created_at"])
        cache = Cache(ttl=300)
Meta attribute Type Description
authentication Authentication Auth backend + permission class
filtering Filtering Filter backends, allowed fields, search fields, ordering fields
pagination Pagination Pagination style (page_number or cursor) and page size
serializer Serializer Field visibility for read/write
cache Cache Response caching TTL and vary-on keys
reflect bool Set True to reflect an existing table instead of creating one
table str Override the inferred table name

See the individual sections in Advanced Topics for full details on each option.