Hooks & overridable methods
Three layers of customization: lightweight hooks for common cases, CRUD pieces when you need to change one operation, and full HTTP handlers when you need everything.
Layer 1 — Hooks (recommended)
Async methods that run at fixed points in dispatch. Default implementations are no-ops. Override them on your resource.
async def pre_process(self, request)
Called once per request, after authentication, before body parsing. Set self.queryset_filter, fetch related context, mutate self.allowed_methods based on user role.
async def pre_process(self, request):
if self.user and self.user.get('role') != 'admin':
self.queryset_filter = {'owner_id': self.user['id']}async def before_cache(self, request)
Called only when cache=True, before the Redis cache lookup. Fold extra context into self.cache_key so the cache varies on it.
async def before_cache(self, request):
role = self.user.get('role') if self.user else 'anon'
self.cache_key += f':role={role}'async def hydrate(self, body)
Mutate the parsed JSON body before the handler runs. Useful for normalizing input formats, computing derived fields, stripping invalid data.
async def hydrate(self, body):
if 'email' in body:
body['email'] = body['email'].lower().strip()async def dehydrate(self, row)
Called for every row in the response. Strip secrets, add computed fields, rename keys, format dates.
async def dehydrate(self, row):
row['display_name'] = row.get('name', '').title()
row.pop('internal_token', None)
if row.get('created_at'):
row['created_at_human'] = row['created_at'].strftime('%Y-%m-%d')async def alter_list(self, results)
Reshape the entire list before the meta envelope is added. Sort, dedupe, inject summary rows.
async def alter_list(self, results):
return sorted(results, key=lambda r: r['priority'], reverse=True)async def alter_detail(self, result)
Reshape the entire detail object before serialization.
async def alter_detail(self, result):
result['permissions'] = await fetch_permissions(self.user, self.obj)
return resultasync def post_process(self, response)
Last chance to modify the response before JSON encoding. Returns the modified value.
async def post_process(self, response):
if self.method == 'patch' and self.diff:
await audit_log(self.user, self.diff)
return responseasync def add_m2m(self, result)
Hook for many-to-many relationships. Default is a no-op. BaseTagsResource and BaseCustomResource use it to attach tags / custom attributes.
Layer 2 — CRUD pieces
Override these when you want to keep dispatch but change one operation.
| Method | Runs on | When to override |
|---|---|---|
get_objs(request) | GET list | Custom listing logic; still benefits from pagination/filters/cache |
get_obj(id) | GET detail | Custom detail logic; pre-loaded select_related/prefetch |
create_obj(request, body) | POST | Custom create — keep cache invalidation by calling super() or invalidating yourself |
update_obj(id, body) | PATCH | Custom update — useful when fields need a side effect |
delete_obj(id) | DELETE | Soft-delete, cascade rules, audit |
count() | ?count=true | Replace InnoDB-optimized count |
Listing mechanics
| Method | Effect |
|---|---|
build_filters(request) | Applies filter_fields, search, tags, queryset_filter |
get_filters(request) | Applies ?filter= JSON / segments |
paginate(request) | Reads page and limit from query |
ordenate(request) | Reads ?order_by= and validates against order_fields |
Response shaping
| Method | Effect |
|---|---|
return_results(results) | Wraps list with {meta, objects}, applies normalize_list |
return_result(result) | Filters detail by edit_fields, applies normalize_obj |
serialize(result) | JSON-encodes with the resource's timezone, calls dehydrate/post_process/save_cache |
Cache
| Method | Effect |
|---|---|
save_cache(content) | Writes the response into Redis with TTL and namespace tracking |
invalidate_cache(namespaces) | Drops every key in the given namespaces |
_cache_namespaces(include_detail_id=None) | Builds the list of namespaces to invalidate (e.g. on PATCH) |
Auth & security
| Method | Effect |
|---|---|
_ownership_filter() | Returns extra filter kwargs when owner_field is set |
_authenticate(request) | Resolves the authenticated session (Bearer, X-Api-Key, or cookie — same precedence as _authenticate) |
_enforce_token(request) | Validates X-Token when ENFORCE_TOKEN |
block(identifier) | Adds the IP to the 24h block list |
check_is_blocked(identifier) | Short-circuits dispatch with 403 when the IP is blocked |
Layer 3 — Full handlers
Override the HTTP verb method itself to take total control. You lose the surrounding plumbing (cache, validation, m2m save) — get it back by calling super() or replicating the relevant parts.
| Method | When to override |
|---|---|
get(request) | Full custom behaviour for GET — list and detail combined |
post(request) | Full custom create flow |
patch(request) | Full custom update flow |
delete(request) | Full custom delete flow |
dispatch(request, *args, **kwargs) | Replace the entire pipeline — almost never needed |
Prefer the highest layer that gets the job done. Hooks are cheap, CRUD pieces keep most of dispatch, full handlers throw it all away. Going one level too low is the most common over-engineering trap.
0-mcp by Stamatios Stamou Jr — github.com/ssjunior/0-mcp