Advanced Concepts in Esmerald¶
This section provides a deep technical dive into advanced features of Esmerald. These go beyond simple route handling and API development, showcasing core extensibility mechanisms built into the framework.
This guide is intended for developers building scalable, maintainable, and extensible systems.
Permissions¶
Permissions in Esmerald allow fine-grained control over who can access certain routes.
Permissions are declared using BasePermission
and can be globally or locally applied.
Why use Permissions?¶
- Enforce role-based access control (RBAC)
- Protect endpoints without mixing authorization logic into handlers
- Reuse authorization logic across multiple routes or gateways
Creating a Permission Class¶
from esmerald.permissions import BasePermission
from esmerald import Request
class IsAdmin(BasePermission):
def has_permission(self, request: Request) -> bool: # or async def has_permission(...)
return request.headers.get("X-ADMIN") == "true"
Applying Permissions to a Route¶
from esmerald import get, HTTPException
@get("/admin", permissions=[IsAdmin])
def admin_dashboard() -> dict:
return {"message": "Welcome admin!"}
If IsAdmin
fails, a 403 Forbidden
is returned.
Observables¶
Observables provide an event-driven model in Esmerald to emit and listen for events between components.
Why Observables?¶
- Decouple business logic
- Enable audit logs, metrics, triggers
- Improve maintainability by externalizing side effects
Declaring an Observable¶
from esmerald import observable
@observable(sender=["user_created"])
def handle_user_created(payload: dict) -> None: ...
Emitting an Observable¶
When declaring the sender
automatically will trigger the event for those listening
to act.
from esmerald import observable
@observable(listen=["user_created"])
def handle_user_created() -> None:
# do something here
¶
from esmerald import observable
@observable(listen=["user_created"])
def handle_user_created() -> None:
# do something here
Interceptors¶
Interceptors allow manipulation of request/response flow before or after they are processed by the route handler.
Why Interceptors?¶
- Reusable, non-invasive logic (e.g., logging, metrics, transformation)
- Avoid bloated middlewares or tightly-coupled decorators
Creating an Interceptor¶
from loguru import logger
from esmerald import EsmeraldInterceptor
from lilya.types import Receive, Scope, Send
class LoggingInterceptor(EsmeraldInterceptor):
async def intercept(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
# Log a message here
logger.info("This is my interceptor being called before reaching the handler.")
Applying an Interceptor¶
from esmerald import Esmerald
app = Esmerald(
routes=[],
interceptors=[LoggingInterceptor]
)
Decorators¶
Esmerald supports decorators to extend or modify the behavior of route handlers.
Common Use Cases¶
- Caching
- Retry logic
- Custom validation
- Metrics collection
Example: Timing Decorator¶
import time
from functools import wraps
def timing(fn):
@wraps(fn)
async def wrapper(*args, **kwargs):
start = time.time()
result = await fn(*args, **kwargs)
print(f"Executed {fn.__name__} in {time.time() - start}s")
return result
return wrapper
Apply it to any handler:
@get("/ping")
@timing
def ping() -> dict:
return {"pong": True}
Encoders¶
In Esmerald, encoders enable the framework to understand, encode, and serialize custom objects seamlessly. This flexibility allows developers to integrate various data types without being constrained to specific serialization libraries.
Benefits of Encoders¶
- Flexibility: Integrate custom data types without relying solely on built-in serializers.
- Extensibility: Design and register your own encoders to handle specific serialization needs.
- Future-Proofing: Ensure compatibility with various libraries and frameworks by defining custom serialization logic.
Example: Encoding a Custom Type¶
from typing import Any
from esmerald.encoders import Encoder
class Money:
def __init__(self, amount: float, currency: str):
self.amount = amount
self.currency = currency
class MoneyEncoder(Encoder):
def is_type(self, value: any) -> bool:
return isinstance(value, Money)
def serialize(self, value: Money) -> dict:
return {"amount": value.amount, "currency": value.currency}
def encode(self, annotation: Any, value: Any) -> dict:
return annotation(**value)
- is_type: Checks if the value is an instance of the Money class.
- serialize: Defines how to convert the Money object into a serializable dictionary.
- encode: Brings the data passed and creates a Money object.
Register the Encoder¶
from esmerald import Esmerald
app = Esmerald(
routes=[],
encoders=[MoneyEncoder]
)
Now, any handler returning a Money
object will automatically be encoded.
Extensions (Pluggables)¶
Extensions allow you to add new functionality or integrate third-party systems in a clean, pluggable way.
Use Cases¶
- Database integrations
- Queues (e.g., RabbitMQ, Kafka)
- Third-party APIs (Stripe, Twilio)
Creating an Extension¶
from esmerald import Extension
class MyDBExtension(Extension):
def extend(self, app):
app.state.db = connect_to_database()
Loading an Extension¶
from esmerald import Esmerald, Pluggable
app = Esmerald(
routes=[],
extensions={'my-extension': Pluggable(MyDBExtension)}
)
Now app.state.db
is accessible throughout the app.
More details can be found with a lot more examples to go through.
Summary¶
This document covered:
✅ Permissions for access control ✅ Observables for event-driven communication ✅ Interceptors to wrap request/response ✅ Decorators for reusable logic ✅ Encoders for custom serialization ✅ Extensions for third-party integrations
These features together form a powerful advanced toolkit to build modular and maintainable Esmerald applications.
👉 Ready to supercharge your app with high-performance caching? Continue to caching to learn about Esmerald’s caching system with memory and Redis support.