Skip to content
GitHubDiscord

Web (lexigram-web)

Web layer for Lexigram Framework — ASGI, routing, middleware, and API tooling.


lexigram-web provides an ASP.NET Core-inspired HTTP layer built on Starlette with constructor injection, a Result-to-HTTP bridge that maps domain errors to status codes automatically, first-class middleware, guard, and filter pipelines, and OpenAPI docs auto-generation.


Terminal window
uv add lexigram lexigram-web[granian]
# With optional server backends
uv add "lexigram-web[uvicorn]" # uvicorn
uv add "lexigram-web[hypercorn]" # hypercorn
uv add "lexigram-web[security]" # itsdangerous for signing
uv add "lexigram-web[templates]" # Jinja2 template support
uv add "lexigram-web[websocket]" # WebSocket support
from lexigram import Application
from lexigram.di.module import Module, module
from lexigram.web import Controller, WebModule, WebProvider, get
class HelloController(Controller):
@get("/hello")
async def hello(self) -> dict[str, str]:
return {"message": "Hello from Lexigram"}
@module(
imports=[
WebModule.configure(
controllers=[HelloController],
host="127.0.0.1",
port=8000,
)
]
)
class AppModule(Module):
pass
async def main() -> None:
async with Application.boot(modules=[AppModule]) as app:
web = await app.container.resolve(WebProvider)
web.run_server(host="127.0.0.1", port=8000)
if __name__ == "__main__":
import asyncio
asyncio.run(main())

Zero-config usage: Call WebModule.configure() with no arguments to use all defaults.

application.yaml
web:
server:
host: "0.0.0.0"
port: 8000
workers: 4
cors:
allowed_origins:
- "https://app.example.com"
rate_limit:
enabled: true
default_limit: "100/minute"
Section titled “Option 2 — Profiles + Environment Variables (recommended)”
Terminal window
export LEX_WEB__SERVER__HOST=0.0.0.0
export LEX_WEB__SERVER__PORT=8080
export LEX_WEB__CORS__ALLOWED_ORIGINS='["https://app.example.com"]'
from lexigram.web import WebModule
from lexigram.web.config import WebConfig, ServerConfig, RateLimitConfig
WebModule.configure(
controllers=[UserController, OrderController],
web_config=WebConfig(
server=ServerConfig(host="0.0.0.0", port=8080, workers=4),
rate_limit=RateLimitConfig(
enabled=True,
default_limit=200,
default_window=60,
),
),
)
FieldDefaultEnv varDescription
server.host"127.0.0.1"LEX_WEB__SERVER__HOSTBind host
server.port8000LEX_WEB__SERVER__PORTBind port
server.workers1LEX_WEB__SERVER__WORKERSWorker processes
server.reloadFalseLEX_WEB__SERVER__RELOADAuto-reload on code change
cors.allowed_origins["localhost:3000", ...]LEX_WEB__CORS__ALLOWED_ORIGINSCORS allow-list — wildcards blocked in production
rate_limit.enabledTrueLEX_WEB__RATE_LIMIT__ENABLEDEnable rate limiting
rate_limit.default_limit100LEX_WEB__RATE_LIMIT__DEFAULT_LIMITRequests per window
rate_limit.default_window60LEX_WEB__RATE_LIMIT__DEFAULT_WINDOWWindow in seconds
rate_limit.storage_backend"memory"LEX_WEB__RATE_LIMIT__STORAGE_BACKEND"memory" or "redis"
enable_authFalseLEX_WEB__ENABLE_AUTHEnable built-in auth middleware
api_docs.enabledFalseLEX_WEB__API_DOCS__ENABLEDEnable /docs + /redoc
max_body_size10 MiBLEX_WEB__MAX_BODY_SIZERequest body size limit
MethodDescription
WebModule.configure(controllers, discover, ...)Configure with controllers and server settings
WebModule.stub()No-op module for unit testing
  • Controller pattern — subclass Controller and annotate methods with HTTP decorators
  • Result-to-HTTP bridgeResult[T, DomainError] maps automatically to status codes (404, 422, 403, etc.)
  • HTTP decorators@get, @post, @put, @delete, @patch, @websocket, etc.
  • Auto-discoveryWebModule.configure(discover=["my_app.api.v1"])
  • Middleware pipeline — register ASGI middleware via AbstractMiddleware
  • Exception filtersDefaultExceptionFilter handles DomainError and HTTPError globally
  • Static files, API docs, debug routes — configurable via WebConfig
  • Rate limiting — per-path rules with memory or Redis storage backend
  • Security — CORS wildcard blocked in production, CSRF enabled by default
from lexigram import Application
from lexigram.web import WebModule
async def test_controller():
async with Application.boot(
modules=[WebModule.stub()]
) as app:
web = await app.container.resolve(WebProvider)
assert web.starlette is not None
FileWhat it contains
src/lexigram/web/module.pyWebModule.configure()
src/lexigram/web/di/provider.pyWebProvider boot phases
src/lexigram/web/routing/decorators.pyHTTP decorators (@get, @post, etc.)
src/lexigram/web/routing/result_bridge.pyResultResponseMapper for Result-to-HTTP mapping
src/lexigram/web/config.pyWebConfig, ServerConfig, RateLimitConfig
src/lexigram/web/middleware/__init__.pyAbstractMiddleware base class
src/lexigram/web/filters/__init__.pyException filters