Endpoint Classes
RestEndpoint is the foundation of every LightAPI v2 application. This page covers every aspect of defining and customising endpoint classes.
Defining an endpoint
Annotate class-level attributes with Python types. LightAPI's metaclass turns them into SQLAlchemy columns, Pydantic schema fields, and HTTP handlers simultaneously.
from typing import Optional
from decimal import Decimal
from lightapi import RestEndpoint, Field
class ProductEndpoint(RestEndpoint):
name: str = Field(min_length=1, max_length=100)
slug: str = Field(min_length=1, max_length=100, unique=True, index=True)
price: Decimal = Field(gt=0, decimal_places=2)
stock: int = Field(ge=0, default=0)
description: Optional[str] = None
active: bool = Field(default=True)
Type map
| Python annotation | SQLAlchemy column type | Nullable |
|---|---|---|
str |
String |
No |
int |
Integer |
No |
float |
Float |
No |
bool |
Boolean |
No |
Decimal |
Numeric(scale=N) |
No |
datetime.datetime |
DateTime |
No |
Optional[T] |
same as T, nullable=True |
Yes |
Auto-injected columns
Every RestEndpoint subclass automatically gets:
| Column | Type | Notes |
|---|---|---|
id |
Integer PK |
Never declared, never writeable |
created_at |
DateTime |
Set on INSERT |
updated_at |
DateTime |
Set on INSERT and UPDATE |
version |
Integer |
Optimistic locking — must be included in PUT/PATCH |
Field() extra kwargs
Beyond Pydantic's built-in constraints, LightAPI processes:
| Kwarg | Description |
|---|---|
unique=True |
UNIQUE constraint |
index=True |
Database index |
foreign_key="table.col" |
Foreign key reference |
decimal_places=N |
A Decimal with more than N decimal places is rejected (422) |
exclude=True |
Skip column creation; field is schema-only |
default=<value> |
Column-level default |
Meta inner class
Control authentication, filtering, pagination, serialisation, caching, and more:
from lightapi import (
RestEndpoint,
Authentication, JWTAuthentication, IsAuthenticated,
Filtering, FieldFilter, SearchFilter, OrderingFilter,
Pagination, Serializer, Cache,
)
class ArticleEndpoint(RestEndpoint):
title: str
body: str
published: bool
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"])
cache = Cache(ttl=60)
Meta attribute |
Type | Description |
|---|---|---|
authentication |
Authentication |
Auth backend + permission class/dict |
filtering |
Filtering |
Filter backends and field whitelists |
pagination |
Pagination |
Pagination style and page size |
serializer |
Serializer |
Read/write field sets |
cache |
Cache |
Redis TTL and vary-on params |
reflect |
bool |
Reflect an existing table instead of creating |
table |
str |
Override the inferred table name |
HTTP method mixins
Use HttpMethod mixins to expose only the verbs you need:
from lightapi import RestEndpoint, HttpMethod
class ReadOnlyEndpoint(RestEndpoint, HttpMethod.GET):
name: str
class CreateListEndpoint(RestEndpoint, HttpMethod.GET, HttpMethod.POST):
name: str
class NoDeleteEndpoint(
RestEndpoint,
HttpMethod.GET, HttpMethod.POST,
HttpMethod.PUT, HttpMethod.PATCH,
):
name: str
Requests to disallowed methods return 405 Method Not Allowed.
Available mixins: HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE.
Method overrides
Override any HTTP verb to add custom logic. The signature receives the Starlette Request object.
import json
from starlette.responses import JSONResponse
from lightapi import RestEndpoint
class OrderEndpoint(RestEndpoint):
item: str
quantity: int
def post(self, request):
data = json.loads(request.body())
if data.get("quantity", 0) > 100:
return JSONResponse({"detail": "Max quantity is 100"}, status_code=422)
return self.create(data)
For async engines, define async def overrides and use the _async helpers:
class OrderEndpoint(RestEndpoint):
item: str
quantity: int
async def post(self, request):
data = json.loads(await request.body())
if data.get("quantity", 0) > 100:
return JSONResponse({"detail": "Max quantity is 100"}, status_code=422)
return await self._create_async(data)
Built-in CRUD methods (sync)
| Method | Description |
|---|---|
self.list(request) |
List all rows |
self.retrieve(request, pk) |
Get one row by pk |
self.create(data) |
Insert a row |
self.update(data, pk, partial=False) |
Update with optimistic locking (partial=True for PATCH) |
self.destroy(request, pk) |
Delete a row |
Built-in async CRUD helpers
| Method | Description |
|---|---|
await self._list_async(request) |
Async list |
await self._retrieve_async(request, pk) |
Async get by pk |
await self._create_async(data) |
Async insert |
await self._update_async(data, pk, partial=False) |
Async update (partial=True for PATCH) |
await self._destroy_async(request, pk) |
Async delete |
queryset scoping
Override queryset() to pre-filter the base query for all operations on this endpoint:
from sqlalchemy import select
class PublishedArticleEndpoint(RestEndpoint):
title: str
published: bool
def queryset(self, request):
cls = type(self)
return select(cls._model_class).where(cls._model_class.published == True)
For async engines:
async def queryset(self, request):
cls = type(self)
return select(cls._model_class).where(cls._model_class.published == True)
Table reflection
Set Meta.reflect = True to map an endpoint to an existing database table instead of creating a new one:
from sqlalchemy import create_engine
from lightapi import LightApi, RestEndpoint
class LegacyUserEndpoint(RestEndpoint):
class Meta:
reflect = True
table = "users" # existing table name
engine = create_engine("postgresql://user:pass@localhost/legacy_db")
app = LightApi(engine=engine)
app.register({"/users": LegacyUserEndpoint})
Table name inference
The table name defaults to the lowercased class name with "s" appended:
| Class name | Table name |
|---|---|
UserEndpoint |
userendpoints |
BlogPost |
blogposts |
Article |
articles |
Override with Meta.table = "custom_name" whenever you want a friendlier
name (this is recommended for any endpoint that uses the Endpoint suffix).