Coming from Django
grelmicro is not a web framework and has no ORM. It runs inside FastAPI, Starlette, Litestar or FastStream. What it gives you maps to Django's cache, on_commit, django-ratelimit and the authentication and permission classes of Django REST framework. This page maps each one, says where the behavior differs, and lists what to use for the rest.
Quick reference
| Django | grelmicro | Page |
|---|---|---|
settings.py |
Keyword arguments, GREL_* variables and mounted files, for grelmicro components only |
Configuration |
LOGGING |
grelmicro.log.configure() and GREL_LOG_* |
Logging |
transaction.on_commit() |
Outbox.publish(...) inside the transaction, @outbox.handler |
Outbox |
cache.get_or_set(...) |
TTLCache and @cached |
Cache |
@cache_page(60) |
CachedResponse(ttl=60) on the route |
HTTP cache |
django-ratelimit @ratelimit |
RateLimitedRequests for the app, RateLimited for one route |
Rate limit |
| DRF throttling | RateLimitedRequests(..., trusted=TrustedProxies([...])) |
Rate limit |
DRF authentication_classes |
AuthenticatedRequests with a JWTVerifier |
Authentication |
DRF IsAuthenticated and permission classes |
Every route is authenticated once AuthenticatedRequests is registered, Authenticated(scopes=[...]) for one route |
Authentication |
request.user |
CurrentPrincipal |
Authentication |
| The test client | Your framework's TestClient with micro.fake() |
Testing |
Concept differences
The outbox survives a crash
on_commit runs the callback in the same process after the commit. A crash in between loses it. Outbox.publish(...) writes the message in the same transaction as your rows, and a relay delivers it at least once, with retries and a dead-letter state. The outbox needs PostgreSQL, through asyncpg or a SQLAlchemy AsyncSession. It does not take a Django connection.
Configuration covers grelmicro, not your app
grelmicro reads GREL_* variables only for its own components, and only when GREL_ENV_LOAD is set. Keep your own settings in pydantic-settings, as Advanced configuration shows. Django forbids changing settings at runtime. ExternalConfig applies a changed mounted file to running components on purpose.
The response caps the cache entry
With @cache_page, the decorator's timeout wins over the response's max-age. grelmicro does the opposite: Cache-Control: max-age=30 caps the entry at 30 seconds whatever the ttl. A response marked no-store, no-cache or private is never stored.
A rate limit answers 429
django-ratelimit with block=True raises PermissionDenied, so the caller gets 403. grelmicro answers 429 with RateLimit and Retry-After headers. It also refuses to start without TrustedProxies or a key, so callers behind a proxy never share one bucket.
Middleware is added for you
You order Django's MIDDLEWARE list yourself. In grelmicro, a component such as AuthenticatedRequests adds its own ASGI middleware when micro.install(app) runs, in a fixed order.
Side by side
A side effect after the commit
from django.db import transaction
def sign_up(email):
with transaction.atomic():
user = User.objects.create(email=email)
transaction.on_commit(lambda: send_welcome_email(user.email))
import asyncio
import os
from pydantic import BaseModel
from grelmicro import Grelmicro
from grelmicro.outbox import Message, Outbox
from grelmicro.providers.postgres import PostgresProvider
postgres = PostgresProvider(os.environ["POSTGRES_URL"])
outbox = Outbox(postgres)
micro = Grelmicro(uses=[outbox])
delivered = asyncio.Event()
class WelcomeEmail(BaseModel):
to: str
user_id: int
async def send_email(to: str, idempotency_key: str) -> None:
print(f"welcome email to {to}")
delivered.set()
@outbox.handler(WelcomeEmail)
async def send_welcome(message: Message[WelcomeEmail]) -> None:
assert message.data is not None # a typed handler always gets its model
await send_email(to=message.data.to, idempotency_key=str(message.id))
async def sign_up(email: str) -> None:
async with postgres.client.acquire() as conn, conn.transaction():
user_id = await conn.fetchval(
"INSERT INTO users (email) VALUES ($1) RETURNING id", email
)
await outbox.publish(conn, WelcomeEmail(to=email, user_id=user_id))
async def main() -> None:
async with micro:
await postgres.client.execute(
"CREATE TABLE IF NOT EXISTS users (id serial PRIMARY KEY, email text)"
)
await sign_up("alice@example.com")
async with asyncio.timeout(5):
await delivered.wait()
if __name__ == "__main__":
asyncio.run(main())
A cached page
from django.views.decorators.cache import cache_page
@cache_page(60)
def list_products(request):
return JsonResponse(load_products(), safe=False)
from fastapi import FastAPI
from pydantic import BaseModel
from grelmicro import Grelmicro
from grelmicro.cache import Cache
from grelmicro.http import CachedResponses
from grelmicro.integrations.fastapi import CachedResponse
from grelmicro.providers.redis import RedisProvider
redis = RedisProvider("redis://localhost:6379/0")
micro = Grelmicro(uses=[Cache(redis), CachedResponses()])
app = FastAPI()
micro.install(app)
class Product(BaseModel):
id: int
name: str
@app.get("/products", dependencies=[CachedResponse(ttl=60)])
async def list_products() -> list[Product]:
return await load_products()
async def load_products() -> list[Product]:
return [Product(id=1, name="Anvil")]
A rate limit
from django_ratelimit.decorators import ratelimit
@ratelimit(key="ip", rate="100/m", block=True)
def list_orders(request):
...
from fastapi import FastAPI
from grelmicro import Grelmicro
from grelmicro.http import ErrorResponses, RateLimitedRequests
from grelmicro.providers.redis import RedisProvider
from grelmicro.resilience import RateLimiter, RateLimiterComponent
from grelmicro.security import TrustedProxies
redis = RedisProvider("redis://localhost:6379/0")
burst = RateLimiter.sliding_window("burst", limit=100, window=60)
daily = RateLimiter.sliding_window("daily", limit=10000, window=86400)
micro = Grelmicro(
uses=[
RateLimiterComponent(redis),
ErrorResponses(),
RateLimitedRequests(
burst,
daily,
trusted=TrustedProxies(["10.0.0.0/8"]),
exclude=("/livez", "/readyz"),
),
]
)
app = FastAPI()
micro.install(app)
Authentication and permissions
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
from fastapi import FastAPI
from grelmicro import Grelmicro
from grelmicro.http import AuthenticatedRequests, ErrorResponses
from grelmicro.integrations.fastapi import (
Anonymous,
Authenticated,
CurrentPrincipal,
)
from grelmicro.security import JWTVerifier
micro = Grelmicro(
uses=[
ErrorResponses(),
AuthenticatedRequests(
JWTVerifier.discover(
"https://auth.example.com/", audience="orders-api"
),
exclude=("/livez", "/readyz"),
),
]
)
app = FastAPI()
@app.get("/orders")
async def list_orders(principal: CurrentPrincipal) -> dict[str, str | None]:
return {"caller": principal.subject}
@app.delete(
"/orders/{order_id}",
dependencies=[Authenticated(scopes=["orders:write"])],
)
async def cancel(order_id: int) -> dict[str, int]:
return {"cancelled": order_id}
@app.get("/catalog", dependencies=[Anonymous()])
async def catalog() -> list[str]:
return ["tea", "coffee"]
micro.install(app)
False friends
| Word | In Django | In grelmicro |
|---|---|---|
| signals | An in-process dispatcher, such as post_save |
Operating system signals such as SIGTERM, which the server handles |
| middleware | The MIDDLEWARE list you order |
ASGI middleware a component adds through micro.install(app) |
| fixtures | Database rows loaded by TestCase.fixtures |
pytest fixtures |
| tags | Template tags | Cache tags for bulk invalidation |
| container | Usually a Docker container | The Grelmicro app that opens and closes your components, or a Docker container |
| channels | Django Channels, for WebSockets | Where a configuration warning shows up: as a Python warning and in the log |
No equivalent, do this instead
- The ORM: use SQLAlchemy or SQLModel.
Outbox.publishtakes theirAsyncSession. - Signals: call the function. Use the Outbox for work after a commit.
- Management commands: use argparse, Typer or Click.
python -m grelmicro checkchecks your wiring in CI. - Object-level permissions: check ownership in the handler, with
CurrentPrincipal. - Login, sessions and the admin: grelmicro verifies tokens from your identity provider. It issues none.
- A task queue: use Celery, Dramatiq or taskiq. grelmicro runs scheduled tasks inside the service.
- An outbox on MySQL or SQLite: the outbox supports PostgreSQL only.