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
Or as an ASGI app (e.g. with Gunicorn + Uvicorn workers):
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:
Declarative format (recommended)
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
Full-featured YAML example
# 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
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.
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:
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:
See Async Support for the full guide.
CORS
Pass a list of allowed origins to enable CORS headers on every response:
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.