Skip to content

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.publish takes their AsyncSession.
  • Signals: call the function. Use the Outbox for work after a commit.
  • Management commands: use argparse, Typer or Click. python -m grelmicro check checks 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.